fix: enforce cleanup and secret integrity
test / image-input-builds (pull_request) Failing after 11m32s
test / unit (pull_request) Has started running
test / coverage (pull_request) Blocked by required conditions
test / integration-docker (pull_request) Blocked by required conditions
tracker-policy-pr / check-pr (pull_request) Failing after 12m21s
test / image-input-builds (pull_request) Failing after 11m32s
test / unit (pull_request) Has started running
test / coverage (pull_request) Blocked by required conditions
test / integration-docker (pull_request) Blocked by required conditions
tracker-policy-pr / check-pr (pull_request) Failing after 12m21s
This commit is contained in:
@@ -17,6 +17,7 @@ from bot_bottle.cli.commands import cleanup as cmd
|
||||
def _make_backend(empty: bool = True):
|
||||
backend = MagicMock()
|
||||
plan = MagicMock(empty=empty)
|
||||
plan.intersect.return_value = plan
|
||||
backend.prepare_cleanup.return_value = plan
|
||||
backend.cleanup = MagicMock()
|
||||
return backend, plan
|
||||
@@ -135,10 +136,12 @@ class TestCmdCleanup(unittest.TestCase):
|
||||
docker.cleanup.assert_called_once_with(docker_plan)
|
||||
fc.cleanup.assert_not_called()
|
||||
|
||||
def test_executes_refreshed_plan_after_confirmation(self):
|
||||
def test_executes_only_displayed_resources_still_current(self):
|
||||
backend = MagicMock()
|
||||
preview = MagicMock(empty=False)
|
||||
refreshed = MagicMock(empty=False)
|
||||
approved = MagicMock(empty=False)
|
||||
preview.intersect.return_value = approved
|
||||
backend.prepare_cleanup.side_effect = [preview, refreshed]
|
||||
|
||||
with patch.object(
|
||||
@@ -152,7 +155,8 @@ class TestCmdCleanup(unittest.TestCase):
|
||||
):
|
||||
self.assertEqual(0, cmd.cmd_cleanup([]))
|
||||
|
||||
backend.cleanup.assert_called_once_with(refreshed)
|
||||
preview.intersect.assert_called_once_with(refreshed)
|
||||
backend.cleanup.assert_called_once_with(approved)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -167,11 +167,11 @@ class TestCleanupRemoval(unittest.TestCase):
|
||||
with patch.object(fc_cleanup, "prepare_cleanup", return_value=plan), \
|
||||
patch.object(fc_cleanup, "_run_root", return_value=Path("/run")), \
|
||||
patch.object(fc_cleanup, "_terminate_orphan") as terminate, \
|
||||
patch.object(fc_cleanup.shutil, "rmtree") as rmtree, \
|
||||
patch("bot_bottle.backend.cleanup_control.shutil.rmtree") as rmtree, \
|
||||
patch.object(fc_cleanup, "info"):
|
||||
fc_cleanup.cleanup(plan)
|
||||
terminate.assert_called_once_with(101, Path("/run"))
|
||||
rmtree.assert_called_once_with("/run/dev-x", ignore_errors=True)
|
||||
rmtree.assert_called_once_with(Path("/run/dev-x"))
|
||||
|
||||
def test_cleanup_skips_resources_no_longer_in_refreshed_plan(self):
|
||||
preview = FirecrackerBottleCleanupPlan(
|
||||
@@ -181,7 +181,7 @@ class TestCleanupRemoval(unittest.TestCase):
|
||||
fc_cleanup, "prepare_cleanup",
|
||||
return_value=FirecrackerBottleCleanupPlan(),
|
||||
), patch.object(fc_cleanup, "_terminate_orphan") as terminate, \
|
||||
patch.object(fc_cleanup.shutil, "rmtree") as rmtree:
|
||||
patch("bot_bottle.backend.cleanup_control.shutil.rmtree") as rmtree:
|
||||
fc_cleanup.cleanup(preview)
|
||||
terminate.assert_not_called()
|
||||
rmtree.assert_not_called()
|
||||
|
||||
@@ -486,6 +486,19 @@ class TestMalformedStatusHeader(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(500, status)
|
||||
|
||||
def test_backend_timeout_returns_503(self):
|
||||
with mock.patch(
|
||||
"bot_bottle.gateway.git_gate.http_backend.subprocess.run",
|
||||
side_effect=subprocess.TimeoutExpired(["git", "http-backend"], 1),
|
||||
):
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{self._port}/repo.git/info/refs",
|
||||
method="GET",
|
||||
)
|
||||
with self.assertRaises(urllib.error.HTTPError) as raised:
|
||||
urllib.request.urlopen(req, timeout=3)
|
||||
self.assertEqual(503, raised.exception.code)
|
||||
|
||||
|
||||
class TestContentLengthBounds(unittest.TestCase):
|
||||
"""PRD 0041: malformed or oversized Content-Length is rejected before
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from bot_bottle.backend import EnumerationError
|
||||
from bot_bottle.backend.cleanup_control import CleanupError
|
||||
from bot_bottle.backend.macos_container import cleanup, enumerate as enum_mod
|
||||
from bot_bottle.backend.macos_container.bottle_cleanup_plan import (
|
||||
MacosContainerBottleCleanupPlan,
|
||||
@@ -31,7 +32,10 @@ class TestMacosContainerCleanup(unittest.TestCase):
|
||||
containers=("bot-bottle-a",),
|
||||
networks=("bot-bottle-net-a",),
|
||||
)
|
||||
with patch.object(cleanup.subprocess, "run") as run:
|
||||
completed = cleanup.subprocess.CompletedProcess(
|
||||
args=[], returncode=0, stdout="", stderr="",
|
||||
)
|
||||
with patch.object(cleanup.subprocess, "run", return_value=completed) as run:
|
||||
cleanup.cleanup(plan)
|
||||
self.assertEqual(
|
||||
["container", "delete", "--force", "bot-bottle-a"],
|
||||
@@ -42,6 +46,18 @@ class TestMacosContainerCleanup(unittest.TestCase):
|
||||
run.call_args_list[1].args[0],
|
||||
)
|
||||
|
||||
def test_cleanup_attempts_all_resources_then_raises(self):
|
||||
plan = MacosContainerBottleCleanupPlan(
|
||||
containers=("bot-bottle-a",), networks=("bot-bottle-net-a",),
|
||||
)
|
||||
failed = cleanup.subprocess.CompletedProcess(
|
||||
args=[], returncode=1, stdout="", stderr="unavailable",
|
||||
)
|
||||
with patch.object(cleanup.subprocess, "run", return_value=failed) as run, \
|
||||
self.assertRaisesRegex(CleanupError, "unavailable"):
|
||||
cleanup.cleanup(plan)
|
||||
self.assertEqual(2, run.call_count)
|
||||
|
||||
def test_container_enumeration_failure_aborts(self):
|
||||
completed = cleanup.subprocess.CompletedProcess(
|
||||
args=[], returncode=1, stdout="", stderr="service unavailable",
|
||||
|
||||
@@ -224,6 +224,17 @@ class TestAgentSecrets(unittest.TestCase):
|
||||
reopened = RegistryStore(self.db)
|
||||
self.assertEqual({"K": "v"}, reopened.get_agent_secrets("bottle-1"))
|
||||
|
||||
def test_v6_migration_clears_legacy_secret_rows(self) -> None:
|
||||
self.store.store_agent_secrets("bottle-1", {"K": "legacy"})
|
||||
with closing(sqlite3.connect(self.db)) as conn:
|
||||
conn.execute(
|
||||
"UPDATE schema_versions SET version = 5 "
|
||||
"WHERE module = 'orchestrator_registry'"
|
||||
)
|
||||
conn.commit()
|
||||
self.store.migrate()
|
||||
self.assertEqual({}, self.store.get_agent_secrets("bottle-1"))
|
||||
|
||||
|
||||
class TestReapAbsent(unittest.TestCase):
|
||||
"""`reap_absent` — the self-heal for rows whose bottle is gone.
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import unittest
|
||||
|
||||
from bot_bottle.orchestrator.store.secret_store import (
|
||||
@@ -83,16 +81,21 @@ class TestDecryptErrors(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, "authentication failed"):
|
||||
decrypt_value(self.secret, tampered)
|
||||
|
||||
def test_reads_legacy_ciphertext_for_migration(self) -> None:
|
||||
key = base64.urlsafe_b64decode(self.secret + "==")
|
||||
nonce = b"0123456789abcdef"
|
||||
plaintext = b"legacy-token"
|
||||
stream = hmac.new(
|
||||
key, nonce + (0).to_bytes(4, "big"), hashlib.sha256,
|
||||
).digest()
|
||||
ciphertext = bytes(p ^ k for p, k in zip(plaintext, stream))
|
||||
legacy = base64.urlsafe_b64encode(nonce + ciphertext).rstrip(b"=").decode()
|
||||
self.assertEqual("legacy-token", decrypt_value(self.secret, legacy))
|
||||
def test_rejects_legacy_ciphertext(self) -> None:
|
||||
legacy = base64.urlsafe_b64encode(
|
||||
b"0123456789abcdeflegacy-token",
|
||||
).rstrip(b"=").decode()
|
||||
with self.assertRaisesRegex(ValueError, "unsupported ciphertext format"):
|
||||
decrypt_value(self.secret, legacy)
|
||||
|
||||
def test_rejects_authenticated_blob_with_changed_version(self) -> None:
|
||||
raw = bytearray(base64.urlsafe_b64decode(
|
||||
encrypt_value(self.secret, "secret-token") + "=="
|
||||
))
|
||||
raw[0] ^= 1
|
||||
downgraded = base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
||||
with self.assertRaisesRegex(ValueError, "unsupported ciphertext format"):
|
||||
decrypt_value(self.secret, downgraded)
|
||||
|
||||
def test_truncated_blob_raises_value_error(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
|
||||
Reference in New Issue
Block a user