"""Real Compose rendering plus adversarial production-config mutations; stdlib only.""" import copy import importlib.util import json import os from pathlib import Path import shutil import subprocess import tempfile import unittest ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location("ingress", ROOT / "scripts/validate-ingress.py") ingress = importlib.util.module_from_spec(spec) spec.loader.exec_module(ingress) class IngressConfigTests(unittest.TestCase): @classmethod def setUpClass(cls): env = dict(os.environ, AI_SERVICE_TOKEN="synthetic-b06", APP_PUBLIC_BASE_URL="https://jobs.example.test", INGRESS_PROXY_CIDRS="192.0.2.10/32", WEB_PROXY_SUBNET="172.31.250.0/29", NEXT_PUBLIC_API_BASE_URL="/api") with tempfile.NamedTemporaryFile(mode="w", suffix=".env", delete=False) as file: file.write("# No repository secrets are read by this fixture.\n") path = file.name try: # CI installs standalone Compose; developer machines can use the Docker plugin. command = ["docker", "compose"] if shutil.which("docker") else [shutil.which("compose") or "docker-compose"] base = command + ["--env-file", path, "-f", "docker-compose.yml"] def render(extra): result = subprocess.run(base + extra + ["--profile", "bundled-ollama", "config", "--format", "json"], cwd=ROOT, env=env, capture_output=True, text=True, timeout=30, check=True) return json.loads(result.stdout) cls.production = render([]) cls.development = render(["-f", "docker-compose.dev.yml"]) finally: Path(path).unlink() def test_effective_production_is_safe(self): ingress.validate(self.production) def test_development_remains_explicit_and_cannot_pass_production_preflight(self): self.assertEqual(self.development["services"]["backend"]["ports"][0]["published"], "5202") with self.assertRaises(ValueError): ingress.validate(self.development) def test_public_internal_ports_and_trust_bypasses_fail_closed(self): def change(config, service, key, value): config["services"][service][key] = value mutations = [ lambda c: change(c, "backend", "ports", [{"target": 8080, "published": "5202"}]), lambda c: change(c, "ai-service", "ports", [{"target": 8001, "published": "8001"}]), lambda c: change(c, "ollama", "network_mode", "host"), lambda c: c["services"]["frontend"]["ports"].append({"target": 80, "published": "8081"}), lambda c: c["services"]["frontend"]["environment"].update(INGRESS_MODE="direct"), lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS=""), lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS="0.0.0.0/0"), lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS="::/0"), lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS="::/64"), lambda c: c["services"]["frontend"]["environment"].update(INGRESS_PROXY_CIDRS="::ffff:0:0/96"), lambda c: c["services"]["backend"]["environment"].update(ASPNETCORE_ENVIRONMENT="Development"), lambda c: c["services"]["backend"]["environment"].update(DOTNET_ENVIRONMENT="Development"), lambda c: c["services"]["backend"]["environment"].update(ASPNETCORE_FORWARDEDHEADERS_ENABLED="true"), lambda c: c["services"]["backend"]["environment"].update(Proxy__KnownNetworks__0="0.0.0.0/0"), lambda c: c["networks"]["web_proxy"].update(internal=False), lambda c: c["services"]["ai-service"]["networks"].update(web_proxy={}), lambda c: c["services"]["frontend"]["build"]["args"].update(NEXT_PUBLIC_API_BASE_URL="http://localhost:5202/api"), ] for index, mutation in enumerate(mutations): with self.subTest(mutation=index): config = copy.deepcopy(self.production) mutation(config) with self.assertRaises(ValueError): ingress.validate(config) if __name__ == "__main__": unittest.main()