80 lines
4.3 KiB
Python
80 lines
4.3 KiB
Python
"""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:
|
|
# docker/setup-compose-action supplies `compose` on runners without Docker.
|
|
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()
|