Files
bot-bottle/tests/integration/test_gateway_image.py
T

115 lines
4.0 KiB
Python

"""Integration: PRD 0024 chunk 1 — the gateway image builds
and the daemon binaries are present + executable inside it.
This test does NOT exercise the daemons running against real
config (routes.yaml, etc) — that lands in chunk 2 when the
renderer wires the bundle into compose. What we verify here is
the chunk-1 contract:
- Dockerfile.gateway builds (multi-stage works, base layers
pull, COPYs resolve).
- gitleaks, mitmdump are at the documented paths and answer
`--version`.
- The Python init at /app/gateway_init.py runs and prints the
expected "no daemons selected" line when the supervisor is
pointed at an empty daemon set.
Skips cleanly only when the selected Docker backend is unavailable.
"""
from __future__ import annotations
import os
import subprocess
import unittest
from tests._backend import skip_unless_backend
_IMAGE = "bot-bottle-gateway-test:chunk1"
_DOCKERFILE = "Dockerfile.gateway"
@skip_unless_backend("docker")
class TestGatewayImage(unittest.TestCase):
"""Builds the image once for the class, then runs a few
`docker run` probes against it."""
@classmethod
def setUpClass(cls) -> None:
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
proc = subprocess.run(
["docker", "build", "-t", _IMAGE,
"-f", _DOCKERFILE, "."],
cwd=repo_root,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
check=False,
)
if proc.returncode != 0:
raise AssertionError(
f"docker build failed; image probes cannot run.\n"
f"{proc.stdout.decode('utf-8', errors='replace')[-2000:]}"
)
@classmethod
def tearDownClass(cls) -> None:
subprocess.run(
["docker", "image", "rm", "-f", _IMAGE],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=False,
)
def _run_in_image(self, *cmd: str, timeout: float = 30.0) -> tuple[int, str]:
proc = subprocess.run(
["docker", "run", "--rm", "--entrypoint", cmd[0], _IMAGE,
*cmd[1:]],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=timeout,
check=False,
)
return proc.returncode, proc.stdout.decode("utf-8", errors="replace")
def test_gitleaks_binary_present_and_versioned(self):
rc, out = self._run_in_image("/usr/bin/gitleaks", "version")
self.assertEqual(0, rc, msg=out)
# gitleaks prints a bare version string like "v8.x.y".
self.assertRegex(out, r"v?\d+\.\d+")
def test_mitmdump_binary_present_and_versioned(self):
rc, out = self._run_in_image("mitmdump", "--version")
self.assertEqual(0, rc, msg=out)
self.assertIn("Mitmproxy", out)
def test_python_imports_supervise_module(self):
# The supervise daemon is installed as part of the bot_bottle
# package (5ad3449), not as flat sibling modules under /app.
# Probe that the package imports resolve inside the image.
rc, out = self._run_in_image(
"python3", "-c",
"from bot_bottle.supervisor import types; "
"from bot_bottle.gateway.supervisor import server as supervise_server; "
"print('ok')",
)
self.assertEqual(0, rc, msg=out)
self.assertIn("ok", out)
def test_init_supervisor_runs_with_no_daemons(self):
# `nothing` matches no canonical daemon → supervisor exits 0
# immediately with the documented message. Confirms the
# ENTRYPOINT wiring works.
proc = subprocess.run(
["docker", "run", "--rm",
"-e", "BOT_BOTTLE_GATEWAY_DAEMONS=nothing",
_IMAGE],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=10.0,
check=False,
)
out = proc.stdout.decode("utf-8", errors="replace")
self.assertEqual(0, proc.returncode, msg=out)
self.assertIn("no daemons selected", out)
if __name__ == "__main__":
unittest.main()