mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
187 lines
11 KiB
GDScript
187 lines
11 KiB
GDScript
extends "res://tests/test_case.gd"
|
|
|
|
# Task 6.3. The behaviour that matters is not "it parses a port" — it is the
|
|
# precedence order an operator relies on, and the refusal to run on a typo.
|
|
|
|
const ServerConfigScript = preload("res://scripts/server_config.gd")
|
|
|
|
|
|
func _parse(args: Array) -> Variant:
|
|
var argv := PackedStringArray()
|
|
for a in args:
|
|
argv.append(a)
|
|
return ServerConfigScript.parse(argv)
|
|
|
|
|
|
func _temp_config(body: String) -> String:
|
|
var path := "user://test_server_%d.cfg" % Time.get_ticks_usec()
|
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
|
f.store_string(body)
|
|
f.close()
|
|
return path
|
|
|
|
|
|
func test_defaults_apply_when_nothing_is_given() -> void:
|
|
var config = _parse([])
|
|
assert_true(config.is_valid(), "an empty command line is valid")
|
|
assert_eq(config.get_value("port"), 7777, "default port")
|
|
assert_eq(config.get_value("max-matches"), 0, "0 means run forever")
|
|
assert_eq(config.get_value("log-level"), "info", "default log level")
|
|
|
|
|
|
func test_command_line_values_are_typed_not_strings() -> void:
|
|
var config = _parse(["--port=7000", "--match-length=90.5", "--fill-bots"])
|
|
assert_true(config.is_valid(), "valid: %s" % str(config.errors))
|
|
assert_eq(config.get_value("port"), 7000, "int stays an int")
|
|
assert_almost_eq(config.get_value("match-length"), 90.5, 0.001, "float stays a float")
|
|
assert_eq(config.get_value("fill-bots"), true, "a bare bool flag is true")
|
|
|
|
|
|
func test_an_unknown_flag_is_an_error_not_a_shrug() -> void:
|
|
# The whole reason this class exists: `--max-clientss=8` used to run a
|
|
# server on the default cap and say nothing at all.
|
|
var config = _parse(["--max-clientss=8"])
|
|
assert_true(not config.is_valid(), "a typo'd flag is rejected")
|
|
assert_true("max-clientss" in " ".join(config.errors), "and the error names it: %s" % str(config.errors))
|
|
|
|
|
|
func test_a_value_that_is_not_the_declared_type_is_rejected() -> void:
|
|
var config = _parse(["--port=seven"])
|
|
assert_true(not config.is_valid(), "a non-numeric port is rejected")
|
|
var config_ok = _parse(["--port=7000"])
|
|
assert_true(config_ok.is_valid(), "control: a numeric port is accepted")
|
|
|
|
|
|
func test_a_non_bool_flag_without_a_value_is_rejected() -> void:
|
|
# Silently defaulting here would hide a shell-quoting mistake.
|
|
var config = _parse(["--port"])
|
|
assert_true(not config.is_valid(), "--port with no value is an error")
|
|
|
|
|
|
func test_no_prefix_turns_a_bool_off() -> void:
|
|
var config = _parse(["--no-fill-bots"])
|
|
assert_true(config.is_valid(), "valid: %s" % str(config.errors))
|
|
assert_eq(config.get_value("fill-bots"), false, "--no- inverts it")
|
|
# And it must not be listed separately, or --help doubles in length.
|
|
var help: String = ServerConfigScript.help_text()
|
|
assert_eq(help.count("--no-fill-bots"), 1, "--no- form appears once, in the default hint")
|
|
|
|
|
|
func test_the_command_line_beats_the_config_file() -> void:
|
|
var path := _temp_config("[server]\nport=8100\nmax-clients=4\n")
|
|
var config = _parse(["--config=%s" % path, "--port=9200"])
|
|
assert_true(config.is_valid(), "valid: %s" % str(config.errors))
|
|
assert_eq(config.get_value("port"), 9200, "the command line wins")
|
|
assert_eq(config.get_value("max-clients"), 4, "the file still supplies what the command line omits")
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
|
|
|
|
|
func test_the_config_file_beats_the_default() -> void:
|
|
var path := _temp_config("[server]\nport=8100\n")
|
|
var config = _parse(["--config=%s" % path])
|
|
assert_true(config.is_valid(), "valid: %s" % str(config.errors))
|
|
assert_eq(config.get_value("port"), 8100, "the file overrides the default")
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
|
|
|
|
|
func test_a_missing_or_malformed_config_file_is_an_error() -> void:
|
|
var config = _parse(["--config=user://definitely_not_here_%d.cfg" % Time.get_ticks_usec()])
|
|
assert_true(not config.is_valid(), "a config file that cannot be read is an error, not silence")
|
|
var path := _temp_config("[server]\nnonsense=1\n")
|
|
var unknown_key = _parse(["--config=%s" % path])
|
|
assert_true(not unknown_key.is_valid(), "an unknown key in the file is rejected too")
|
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
|
|
|
|
|
func test_out_of_range_values_are_rejected_with_their_own_message() -> void:
|
|
assert_true(not _parse(["--port=0"]).is_valid(), "port 0 is out of range")
|
|
assert_true(not _parse(["--port=70000"]).is_valid(), "port 70000 is out of range")
|
|
assert_true(not _parse(["--max-clients=0"]).is_valid(), "a server for nobody is rejected")
|
|
assert_true(not _parse(["--match-length=0"]).is_valid(), "a zero-length match is rejected")
|
|
assert_true(not _parse(["--log-level=chatty"]).is_valid(), "an undefined log level is rejected")
|
|
assert_true(not _parse(["--arena-rotation=spiral"]).is_valid(), "an undefined rotation mode is rejected")
|
|
assert_true(not _parse(["--arena-path=res://scenes/arena_01_elevated.tscn"]).is_valid(), "an elevated arena cannot be selected for allocated ranked play")
|
|
assert_true(_parse(["--arena-path=res://scenes/arena_01.tscn"]).is_valid(), "a ranked-eligible arena path is accepted")
|
|
assert_true(not _parse(["--smoke-force-goal-after=-2"]).is_valid(), "only -1 disables the deterministic smoke goal")
|
|
# Control: the same flags at legal values all pass together.
|
|
var ok = _parse(["--port=7000", "--max-clients=6", "--match-length=90", "--log-level=warn", "--arena-rotation=random"])
|
|
assert_true(ok.is_valid(), "control: legal values pass (%s)" % str(ok.errors))
|
|
|
|
|
|
func test_a_repeated_flag_is_rejected_rather_than_last_wins() -> void:
|
|
# Last-wins hides a duplicated line in a generated systemd unit.
|
|
var config = _parse(["--port=7000", "--port=8000"])
|
|
assert_true(not config.is_valid(), "the same flag twice is an error")
|
|
|
|
|
|
func test_help_documents_every_declared_flag() -> void:
|
|
# The acceptance criterion is literally "--help documents every flag", so
|
|
# assert it against the declaration rather than against a hand-kept list.
|
|
var help: String = ServerConfigScript.help_text()
|
|
for spec in ServerConfigScript.specs():
|
|
assert_true("--%s" % spec.key in help, "--%s appears in --help" % spec.key)
|
|
assert_true(spec.help in help, "and so does its description")
|
|
|
|
|
|
func test_help_is_requested_without_needing_a_valid_command_line() -> void:
|
|
var config = _parse(["--help"])
|
|
assert_true(config.help_requested, "--help is recognised")
|
|
var short = _parse(["-h"])
|
|
assert_true(short.help_requested, "-h too")
|
|
|
|
|
|
func test_allocated_mode_is_opt_in_and_requires_compatibility_manifest() -> void:
|
|
var community = _parse([])
|
|
assert_true(community.is_valid(), "community defaults remain valid")
|
|
assert_eq(community.get_value("allocated-mode"), false, "allocation is opt-in")
|
|
var incomplete = _parse(["--allocated-mode", "--transport=enet"])
|
|
assert_true(not incomplete.is_valid(), "allocated mode cannot start without its manifest")
|
|
var valid = _parse([
|
|
"--allocated-mode", "--match-id=match_1234567890123456", "--server-id=server_1234567890123456",
|
|
"--playlist-version=2026-08-31", "--client-build=client-2026-08-31", "--assignment-expiry-unix=%d" % (Time.get_unix_time_from_system() + 3600), "--server-image-digest=sha256:" + "a".repeat(64),
|
|
"--playlist=casual", "--transport=enet", "--region=EU", "--join-authorisations-file=/run/secrets/join-authorisations.json", "--join-authorisations-key-file=/run/secrets/join-authorisations.key"
|
|
])
|
|
assert_true(valid.is_valid(), "a complete allocated compatibility manifest is accepted: %s" % str(valid.errors))
|
|
|
|
|
|
func test_allocated_mode_rejects_invalid_transport_region_or_digest() -> void:
|
|
var args := [
|
|
"--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v",
|
|
"--client-build=client", "--assignment-expiry-unix=%d" % (Time.get_unix_time_from_system() + 3600),
|
|
"--server-image-digest=sha256:" + "g".repeat(64), "--transport=udp", "--region=AP"
|
|
]
|
|
var config = _parse(args)
|
|
assert_true(not config.is_valid(), "invalid compatibility values are rejected")
|
|
var unsafe_id = _parse([
|
|
"--allocated-mode", "--match-id=short", "--server-id=server/unsafe", "--playlist-version=v",
|
|
"--client-build=client", "--assignment-expiry-unix=%d" % (Time.get_unix_time_from_system() + 3600),
|
|
"--server-image-digest=sha256:" + "a".repeat(64), "--playlist=casual", "--transport=enet", "--region=EU",
|
|
"--join-authorisations-file=/run/secrets/join-authorisations.json", "--join-authorisations-key-file=/run/secrets/join-authorisations.key"
|
|
])
|
|
assert_true(not unsafe_id.is_valid(), "short or unsafe allocated identifiers are rejected")
|
|
|
|
|
|
func test_allocated_mode_rejects_missing_or_expired_assignment_manifest_fields() -> void:
|
|
var missing = _parse(["--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU"])
|
|
assert_true(not missing.is_valid(), "client build and expiry are required")
|
|
var expired = _parse(["--allocated-mode", "--match-id=m", "--server-id=s", "--playlist-version=v", "--client-build=client", "--assignment-expiry-unix=1", "--server-image-digest=sha256:" + "a".repeat(64), "--transport=enet", "--region=EU"])
|
|
assert_true(not expired.is_valid(), "expired assignment is rejected")
|
|
|
|
|
|
func test_allocated_start_floor_is_the_verified_roster_size() -> void:
|
|
var boot = preload("res://scripts/server_boot.gd")
|
|
assert_eq(boot.required_min_players(true, 6, 1), 6, "allocated six-player roster cannot start with one player")
|
|
assert_eq(boot.required_min_players(true, 2, 6), 2, "allocated casual roster uses its complete size")
|
|
assert_eq(boot.required_min_players(false, 1, 1), 1, "direct server keeps its configured floor")
|
|
|
|
|
|
func test_connection_reporting_requires_safe_workload_configuration() -> void:
|
|
var boot = preload("res://scripts/server_boot.gd")
|
|
assert_true(boot.valid_connection_report_configuration("http://control-plane:8080", "signed-token", "match-1234567890", "server-123456789", "player-123456789"), "allocated workload configuration is accepted")
|
|
assert_true(not boot.valid_connection_report_configuration("", "signed-token", "match-1234567890", "server-123456789", "player-123456789"), "allocated startup fails closed without a control-plane lease URL")
|
|
assert_true(not boot.valid_connection_report_configuration("http://control-plane:8080", "", "match-1234567890", "server-123456789", "player-123456789"), "allocated startup fails closed without a workload lease credential")
|
|
assert_true(not boot.valid_connection_report_configuration("http://control-plane:8080?token=leak", "signed-token", "match-1234567890", "server-123456789", "player-123456789"), "query-bearing control-plane URL is rejected")
|
|
assert_true(not boot.valid_connection_report_configuration("http://control-plane:8080", "token\nforged", "match-1234567890", "server-123456789", "player-123456789"), "header injection token is rejected")
|
|
assert_true(not boot.valid_connection_report_configuration("http://control-plane:8080", "signed-token", "short", "server-123456789", "player-123456789"), "non-opaque match identity is rejected")
|
|
assert_true(not boot.valid_connection_report_configuration("http://control-plane:8080", "signed-token", "match-1234567890", "server-123456789", "short"), "non-opaque player identity is rejected")
|