diff --git a/scripts/verify_multiplayer_local.sh b/scripts/verify_multiplayer_local.sh index aa5bd7d2..d4adfa18 100755 --- a/scripts/verify_multiplayer_local.sh +++ b/scripts/verify_multiplayer_local.sh @@ -50,12 +50,21 @@ run_godot_harness echo "local multiplayer gate: contracts and manifests" python3 -m json.tool "$root_dir/server/contracts/v1/openapi.json" >/dev/null +# json.tool only proves the contract parses. test_contracts.py is what actually +# checks the operation IDs, envelopes and state vocabulary generated clients +# bind to; it was previously not run by any target, so a real mismatch between +# openapi.json and the suite sat undetected. +python3 "$root_dir/server/contracts/v1/test_contracts.py" python3 "$root_dir/server/migrations/test_migration.py" python3 "$root_dir/server/security/test_fleet_manifests.py" python3 "$root_dir/server/security/test_compose_manifests.py" python3 "$root_dir/server/security/test_kubernetes_policies.py" python3 "$root_dir/server/security/test_supply_chain.py" +python3 "$root_dir/server/security/test_threat_model.py" python3 "$root_dir/scripts/verify_observability_manifests.py" +# The checker above validates the checked-in manifests; this validates the +# checker itself still rejects a widened scrape scope. +python3 "$root_dir/server/security/test_observability_manifests.py" python3 -m unittest "$root_dir/scripts/test_verify_agones_allocation_response.py" echo "LOCAL MULTIPLAYER GATE PASS" diff --git a/server/contracts/v1/test_contracts.py b/server/contracts/v1/test_contracts.py index fbfbe9be..cb172160 100644 --- a/server/contracts/v1/test_contracts.py +++ b/server/contracts/v1/test_contracts.py @@ -23,12 +23,17 @@ class ContractTest(unittest.TestCase): for operation in path.values() if isinstance(operation, dict) and "operationId" in operation } - self.assertTrue({ + # These are the operation IDs generated clients bind to, so a rename + # here is a breaking change for every consumer. Assert the difference + # rather than a bare subset check: a plain assertTrue reports only + # "False is not true" and hides which operation went missing. + required = { "createSteamSession", "getProfile", "createQueueTicket", "heartbeatQueueTicket", "cancelQueueTicket", "acceptProposal", "declineProposal", "getAssignment", "registerServer", - "recordPlayerConnected", "submitMatchResult", "getRankedProfile", - } <= operations) + "claimPlayerConnection", "submitMatchResult", "getRankedProfile", + } + self.assertEqual(set(), required - operations) def test_ranked_profile_contract_is_authoritative_and_optional_season_metadata(self): schema = self.openapi["components"]["schemas"]["RankedProfile"] diff --git a/server/security/test_observability_manifests.py b/server/security/test_observability_manifests.py index efe3551a..e5955253 100644 --- a/server/security/test_observability_manifests.py +++ b/server/security/test_observability_manifests.py @@ -20,17 +20,50 @@ class ObservabilityManifestTest(unittest.TestCase): result = self.run_checker() self.assertEqual(result.returncode, 0, result.stderr) - def test_checker_rejects_wrong_namespace_and_broad_scrape(self): + # Every file the checker opens, in the order it opens them. Copying only a + # subset makes it die on a missing file before it reaches the assertion + # under test, so the mutation is never actually exercised. + FIXTURE = ( + "kustomization.yaml", + "prometheus-service-monitor.yaml", + "prometheus-allocator-service-monitor.yaml", + "prometheus-rules.yaml", + ) + + def build_fixture(self, target, mutate=None): + for name in self.FIXTURE: + text = (ROOT / "deploy/observability" / name).read_text() + if mutate is not None and name == "prometheus-service-monitor.yaml": + text = mutate(text) + (target / name).write_text(text) + + def test_unmutated_fixture_copy_passes(self): + # Guards the two mutation tests below: if this fails, their non-zero + # exit proves nothing, because the fixture itself is broken. with tempfile.TemporaryDirectory() as directory: target = Path(directory) - for name in ("prometheus-service-monitor.yaml", "prometheus-rules.yaml"): - (target / name).write_text((ROOT / "deploy/observability" / name).read_text()) - monitor = target / "prometheus-service-monitor.yaml" - monitor.write_text(monitor.read_text().replace("path: /metrics", "path: /").replace("- cosmic-clash", "- default")) + self.build_fixture(target) + result = self.run_checker(target) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_checker_rejects_wrong_namespace(self): + with tempfile.TemporaryDirectory() as directory: + target = Path(directory) + # Widen the namespaceSelector only; metadata.namespace stays put so + # this isolates the scrape-scope check from the placement check. + self.build_fixture(target, lambda text: text.replace(" - cosmic-clash", " - default")) result = self.run_checker(target) self.assertNotEqual(result.returncode, 0) self.assertIn("namespace", result.stderr) + def test_checker_rejects_broad_scrape_path(self): + with tempfile.TemporaryDirectory() as directory: + target = Path(directory) + self.build_fixture(target, lambda text: text.replace(" path: /metrics", " path: /")) + result = self.run_checker(target) + self.assertNotEqual(result.returncode, 0) + self.assertIn("path", result.stderr) + if __name__ == "__main__": unittest.main()