# Tests Plain-Python test suite using stdlib `unittest`. No external dependencies. Unit tests run anywhere Python 3 is present; integration tests run through the backend named by `BOT_BOTTLE_BACKEND` (default `docker`) and skip cleanly when that backend isn't available on the host. ## Layout ``` tests/ fixtures.py # JSON manifest builders (shared) _backend.py # backend selection + skip guards (shared) unit/ test_egress.py test_egress_addon_core.py test_manifest_egress.py test_dlp_detectors.py test_manifest_runtime.py ... # many others; see unit/ directory integration/ test_gateway_image.py test_dry_run_plan.py test_orphan_cleanup.py ... canaries/ # opt-in; see below (currently empty) ``` Classification falls out of the directory — no hand-maintained list to keep in sync. ## Running ```bash python -m unittest discover -t . -s tests/unit -v # unit only python -m unittest discover -t . -s tests/integration -v # integration only python -m unittest discover -t . -s tests -v # both (recursive) python -m unittest tests.unit.test_manifest_egress # one file ``` Discovery is invoked with `-t .` (top-level dir = repo root) so the `bot_bottle` package on `sys.path` resolves correctly. ## What the integration tests cover - `test_dry_run_plan.py` — `cli.py start --dry-run --format=json` emits a structured plan that contains the resolved egress allowlist and the bottle's runtime, and creates zero Docker resources. - `test_orphan_cleanup.py` — `network_remove` is idempotent against missing resources, so the EXIT trap can call it unconditionally. - `test_gateway_image.py` — builds Dockerfile.gateway and probes that gitleaks / mitmdump / supervise are all reachable inside the gateway image. ## Canaries `tests/canaries/` holds upstream-regression checks gated on `BOT_BOTTLE_RUN_CANARIES=1` and not part of the per-push suite. They're invoked by the scheduled `canaries` workflow. Currently no canaries are defined. ```bash BOT_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v ``` ## What's NOT covered - `bot_bottle/ssh.py` end-to-end (would need a fake SSH host inside the container). - A live SSH-through-git-gate tunnel against a real Tailscale-style IP. - DLP false-positive measurements. - TLS handling / cert pinning behavior. ## Adding a test 1. Pick the directory: `tests/unit/` for a pure unit test, `tests/integration/` for one that needs a backend. 2. Filename: `test_.py`. 3. Boilerplate: ```python import unittest from bot_bottle. import class TestThing(unittest.TestCase): def test_x(self): ... if __name__ == "__main__": unittest.main() ``` 4. Skip guards live in `tests._backend` and gate on the backend's own readiness check, `bot_bottle.backend.has_backend` — the same probe behind `./cli.py backend status`: - Backend-agnostic tests (go through `get_bottle_backend()`) decorate the class with `@skip_unless_selected_backend_available()` — the test runs against whichever backend `BOT_BOTTLE_BACKEND` selects and skips unless that backend is available (checking, e.g., Linux + `/dev/kvm` for Firecracker rather than unrelated Docker availability). - Backend-specific tests (exercise `DockerBroker`, `DockerGateway`, `backend.docker.*`, …) decorate with `@skip_unless_backend("docker")` so they no-op under a run targeting a different backend. Each CI integration job runs `./cli.py backend status --backend=` as a preflight, which prints a clear per-check summary and exits non-zero when the backend is missing — so absent infrastructure fails the job instead of hiding among per-test `unittest.skip` lines.