This commit is contained in:
+127
-2
@@ -1650,10 +1650,12 @@ class AgentTests(unittest.TestCase):
|
||||
self.assertEqual(self.agent.execute(self.operation("instance.remove"))["state"], "failed")
|
||||
self.assertFalse(any(call[:2] == ("container", "stop") for call in self.docker.calls))
|
||||
|
||||
def test_private_bank_file_survives_stale_file_plans_without_snapshots(self):
|
||||
def test_private_moderation_file_survives_stale_file_plans_without_snapshots(self):
|
||||
self.test_private_bank_file_survives_stale_file_plans_without_snapshots(value=module.MODERATION_CONFIG_PATH)
|
||||
|
||||
def test_private_bank_file_survives_stale_file_plans_without_snapshots(self, value="configs/electric_cherry_bank.cfg"):
|
||||
self.create()
|
||||
app = self.agent.instance_dir(INSTANCE, 1) / "app"
|
||||
value = "configs/electric_cherry_bank.cfg"
|
||||
target = app / value
|
||||
secret = b'set tws_bank_token "ec_bank_private_regression"\n'
|
||||
target.write_bytes(secret)
|
||||
@@ -1707,6 +1709,129 @@ class AgentTests(unittest.TestCase):
|
||||
with self.assertRaises(AgentError):
|
||||
self.agent.initial_files(payload)
|
||||
|
||||
def moderation_configuration(self, generation=1):
|
||||
return {"token": "ec_moderation_" + "m" * 43, "configPath": module.MODERATION_CONFIG_PATH,
|
||||
"url": "https://public-controller.example/api/moderation/runtime", "generation": generation}
|
||||
|
||||
def create_moderation_bootstrap(self, *, direct=False, game="t5"):
|
||||
payload = {"name": "server", "game": game, "libraryId": self.library_id,
|
||||
"image": "ghcr.io/ayymoss/plutainer:latest", "port": 28960, "rconPassword": "secret",
|
||||
"env": {"PLUTAINER_EXTRA_ARGS": "+exec electric_cherry_bank.cfg +map_rotate"},
|
||||
"bank": {"token": "ec_bank_" + "b" * 43, "configPath": "configs/electric_cherry_bank.cfg"}}
|
||||
if direct:
|
||||
payload["moderation"] = self.moderation_configuration()
|
||||
payload["env"]["PLUTAINER_EXTRA_ARGS"] = "+exec electric_cherry_moderation.cfg +map_rotate"
|
||||
row = self.agent.execute(self.operation("instance.create", payload, "create"))
|
||||
self.assertEqual(row["state"], "completed", row)
|
||||
return payload
|
||||
|
||||
def test_t6_create_materializes_private_moderation_configuration(self):
|
||||
self.prepare_t6_library()
|
||||
self.test_create_materializes_private_moderation_configuration(game="t6")
|
||||
|
||||
def test_create_materializes_private_moderation_configuration(self, game="t5"):
|
||||
payload = self.create_moderation_bootstrap(direct=True, game=game)
|
||||
target = self.agent.instance_dir(INSTANCE, 1) / "app" / module.MODERATION_CONFIG_PATH
|
||||
self.assertEqual(target.read_bytes(), self.agent.moderation_config(payload["moderation"], 1))
|
||||
self.assertNotIn(self.config["controller"], target.read_text())
|
||||
self.assertIn("moderation.configure", self.agent.inventory()["capabilities"])
|
||||
self.assertNotIn(payload["moderation"]["token"], self.agent.journal.get("create")["result"])
|
||||
|
||||
def test_configure_moderation_persists_existing_startup_and_discards_rcon_secrets(self):
|
||||
saved = self.create_moderation_bootstrap()
|
||||
payload = {"moderation": self.moderation_configuration()}
|
||||
operation = self.operation("instance.configure-moderation", payload)
|
||||
with patch.object(self.agent, "rcon", return_value={"ok": True, "text": payload["moderation"]["token"]}) as command:
|
||||
row = self.agent.execute(operation)
|
||||
self.assertEqual(row["state"], "completed", row)
|
||||
self.assertEqual(self.agent.execute(operation), row)
|
||||
command.assert_called_once_with(INSTANCE, 1, "exec electric_cherry_moderation.cfg")
|
||||
self.assertNotIn(payload["moderation"]["token"], row["result"])
|
||||
app = self.agent.instance_dir(INSTANCE, 1) / "app"
|
||||
self.assertEqual((app / module.MODERATION_CONFIG_PATH).read_bytes(), self.agent.moderation_config(payload["moderation"], 1))
|
||||
bank = (app / "configs/electric_cherry_bank.cfg").read_bytes()
|
||||
self.assertEqual(bank, f'set tws_bank_token "{saved["bank"]["token"]}"\n'.encode() + module.MODERATION_INCLUDE)
|
||||
self.assertEqual(self.agent.spec(INSTANCE, 1), saved)
|
||||
bank = bank.replace(b"\n", b"\r\n")
|
||||
(app / "configs/electric_cherry_bank.cfg").write_bytes(bank)
|
||||
with patch.object(self.agent, "rcon", return_value={"ok": False, "text": "timeout"}):
|
||||
again = self.agent.execute(self.operation("instance.configure-moderation", payload, "configure-again"))
|
||||
self.assertEqual(json.loads(again["result"])["liveLoad"], "unconfirmed")
|
||||
self.assertEqual((app / "configs/electric_cherry_bank.cfg").read_bytes(), bank)
|
||||
self.assertFalse(any(call[:2] == ("container", "restart") for call in self.docker.calls))
|
||||
|
||||
def test_configure_moderation_rejects_unknown_startup_without_writing(self):
|
||||
self.create()
|
||||
with patch.object(self.agent, "rcon") as command:
|
||||
row = self.agent.execute(self.operation("instance.configure-moderation", {"moderation": self.moderation_configuration()}))
|
||||
self.assertEqual(row["state"], "failed", row)
|
||||
self.assertIn("Rebuild this server", row["error"])
|
||||
self.assertFalse((self.agent.instance_dir(INSTANCE, 1) / "app" / module.MODERATION_CONFIG_PATH).exists())
|
||||
command.assert_not_called()
|
||||
|
||||
def test_configure_moderation_requires_readable_verified_private_startup(self):
|
||||
self.create_moderation_bootstrap()
|
||||
app = self.agent.instance_dir(INSTANCE, 1) / "app"
|
||||
bank = app / "configs/electric_cherry_bank.cfg"
|
||||
for index, content in enumerate((None, b'unknown startup commands\n')):
|
||||
with self.subTest(content=content), patch.object(self.agent, "rcon") as command:
|
||||
bank.unlink(missing_ok=True)
|
||||
if content is not None:
|
||||
bank.write_bytes(content)
|
||||
row = self.agent.execute(self.operation("instance.configure-moderation", {"moderation": self.moderation_configuration()}, f"invalid-bootstrap-{index}"))
|
||||
self.assertEqual(row["state"], "failed", row)
|
||||
self.assertIsNone(row["checkpoint"])
|
||||
self.assertFalse((app / module.MODERATION_CONFIG_PATH).exists())
|
||||
command.assert_not_called()
|
||||
|
||||
def test_configure_moderation_validates_credentials_generation_url_and_private_path(self):
|
||||
self.create_moderation_bootstrap()
|
||||
for index, change in enumerate((
|
||||
{"token": 'ec_moderation_' + 'm' * 43 + '\nset other "1"'},
|
||||
{"generation": 2}, {"generation": True}, {"configPath": "configs/other.cfg"},
|
||||
{"url": "http://controller.example/api/moderation/runtime"},
|
||||
{"url": "https://user:secret@controller.example/api/moderation/runtime"},
|
||||
{"url": "https://controller.example/api/moderation/runtime?secret=1"},
|
||||
{"url": 'https://controller.example/api/moderation/runtime";quit'},
|
||||
{"unknown": "value"}
|
||||
)):
|
||||
with self.subTest(change=change), patch.object(self.agent, "rcon") as command:
|
||||
payload = {"moderation": {**self.moderation_configuration(), **change}}
|
||||
row = self.agent.execute(self.operation("instance.configure-moderation", payload, f"bad-moderation-{index}"))
|
||||
self.assertEqual(row["state"], "failed", row)
|
||||
self.assertIsNone(row["checkpoint"])
|
||||
command.assert_not_called()
|
||||
self.assertFalse((self.agent.instance_dir(INSTANCE, 1) / "app" / module.MODERATION_CONFIG_PATH).exists())
|
||||
|
||||
def test_configure_moderation_reconciles_written_configuration_without_replaying_rcon(self):
|
||||
self.create_moderation_bootstrap()
|
||||
operation = self.operation("instance.configure-moderation", {"moderation": self.moderation_configuration()})
|
||||
with patch.object(self.agent, "rcon", side_effect=RuntimeError("secret must not leak")) as command:
|
||||
unknown = self.agent.execute(operation)
|
||||
self.assertEqual(unknown["state"], "unknown", unknown)
|
||||
row = self.agent.execute(operation)
|
||||
self.assertEqual(row["state"], "completed", row)
|
||||
command.assert_called_once()
|
||||
self.assertEqual(json.loads(row["result"])["liveLoad"], "unconfirmed")
|
||||
self.assertNotIn("secret must not leak", str(unknown.get("error")))
|
||||
|
||||
def test_rebuild_rejects_moderation_credentials_for_the_previous_generation_before_disruption(self):
|
||||
saved = self.create_moderation_bootstrap(direct=True)
|
||||
row = self.agent.execute(self.operation("instance.rebuild", {"spec": saved, "nextGeneration": 2}, "rebuild"))
|
||||
self.assertEqual(row["state"], "failed", row)
|
||||
self.assertTrue(self.agent.inspect(INSTANCE, 1)["running"])
|
||||
self.assertFalse(any(call[:2] == ("container", "stop") for call in self.docker.calls))
|
||||
|
||||
def test_rebuild_writes_moderation_credentials_for_only_the_replacement_generation(self):
|
||||
saved = self.create_moderation_bootstrap(direct=True)
|
||||
saved["moderation"] = {**self.moderation_configuration(2), "token": "ec_moderation_" + "n" * 43}
|
||||
row = self.agent.execute(self.operation("instance.rebuild", {"spec": saved, "nextGeneration": 2, "safetyOverride": True}, "rebuild"))
|
||||
self.assertEqual(row["state"], "completed", row)
|
||||
app = self.agent.instance_dir(INSTANCE, 2) / "app"
|
||||
self.assertEqual((app / module.MODERATION_CONFIG_PATH).read_bytes(), self.agent.moderation_config(saved["moderation"], 2))
|
||||
self.assertNotIn(saved["moderation"]["token"], row["result"])
|
||||
self.assertTrue(self.agent.inspect(INSTANCE, 2)["running"])
|
||||
|
||||
def bank_journal(self, decisions=("commit", "cancel"), game="t5", endpoint=module.BANK_GAME_ENDPOINT):
|
||||
path = self.agent.instance_dir(INSTANCE, 1) / f"app/runtime/tws-bank/{game}/{module.digest(endpoint.encode())}.json"
|
||||
reports = [json.dumps({"server": "t5_zombie_test_1", "guid": "ace", "match": "match-one", "request_id": f"{game}-{i}", "operation": "deposit", "amount": 2000, "capacity": 1000000, "decision": decision}) for i, decision in enumerate(decisions)]
|
||||
|
||||
Reference in New Issue
Block a user