Реализация на отдельных двух машинах с протестированным взаимодействием по проверке сообщений
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -11,6 +12,44 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class InfrastructureConfigTests(unittest.TestCase):
|
||||
def test_migrations_pass_one_top_level_statement_per_execute(self) -> None:
|
||||
migration_roots = (
|
||||
ROOT / "api-backend/alembic/versions",
|
||||
ROOT / "bitrix-local-app/alembic/versions",
|
||||
ROOT / "sms-service/migrations/versions",
|
||||
)
|
||||
dollar_quoted = re.compile(
|
||||
r"\$\$.*?\$\$|\$(?P<tag>[A-Za-z_][A-Za-z0-9_]*)\$.*?\$(?P=tag)\$",
|
||||
re.DOTALL,
|
||||
)
|
||||
single_quoted = re.compile(r"'(?:''|[^'])*'", re.DOTALL)
|
||||
|
||||
for migration_root in migration_roots:
|
||||
for migration in migration_root.glob("*.py"):
|
||||
tree = ast.parse(
|
||||
migration.read_text(encoding="utf-8"),
|
||||
filename=str(migration),
|
||||
)
|
||||
for call in ast.walk(tree):
|
||||
if not (
|
||||
isinstance(call, ast.Call)
|
||||
and isinstance(call.func, ast.Attribute)
|
||||
and call.func.attr == "execute"
|
||||
and call.args
|
||||
and isinstance(call.args[0], ast.Constant)
|
||||
and isinstance(call.args[0].value, str)
|
||||
):
|
||||
continue
|
||||
sql = dollar_quoted.sub("DOLLAR_QUOTED_BODY", call.args[0].value)
|
||||
sql = single_quoted.sub("STRING_LITERAL", sql)
|
||||
sql = re.sub(r"--[^\n]*|/\*.*?\*/", "", sql, flags=re.DOTALL)
|
||||
statements = [part for part in sql.split(";") if part.strip()]
|
||||
self.assertLessEqual(
|
||||
len(statements),
|
||||
1,
|
||||
f"{migration}:{call.lineno} passes multiple SQL commands to execute",
|
||||
)
|
||||
|
||||
def test_structlog_event_is_not_passed_twice(self) -> None:
|
||||
log_methods = {"debug", "info", "warning", "error", "exception", "critical"}
|
||||
for source_path in (ROOT / "api-backend/app").rglob("*.py"):
|
||||
@@ -55,7 +94,7 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
jobs = (ROOT / "deployment/docker-compose.jobs.yml").read_text(encoding="utf-8")
|
||||
self.assertEqual(jobs.count("networks: [backend, egress]"), 5)
|
||||
self.assertEqual(jobs.count("networks: [backend, egress]"), 4)
|
||||
|
||||
observability = (ROOT / "observability/docker-compose.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("networks: [observability, egress]", observability)
|
||||
@@ -79,6 +118,107 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
self.assertIn('NGINX_HTTP_PORT:-80}:80', nginx)
|
||||
self.assertIn('NGINX_HTTPS_PORT:-443}:443', nginx)
|
||||
|
||||
def test_production_compose_uses_only_required_digest_images(self) -> None:
|
||||
image_variables = (
|
||||
"API_BACKEND_IMAGE",
|
||||
"BITRIX_LOCAL_APP_IMAGE",
|
||||
"FRONTEND_STATIC_IMAGE",
|
||||
"KEYCLOAK_IMAGE",
|
||||
"NGINX_IMAGE",
|
||||
"OTEL_COLLECTOR_IMAGE",
|
||||
"OTEL_QUEUE_INIT_IMAGE",
|
||||
"REDIS_IMAGE",
|
||||
"SMS_SERVICE_IMAGE",
|
||||
"TOOLBOX_IMAGE",
|
||||
)
|
||||
compose_paths = (
|
||||
ROOT / "infra/compose/application.yml",
|
||||
ROOT / "nginx/docker-compose.yml",
|
||||
ROOT / "redis/docker-compose.yml",
|
||||
ROOT / "observability/docker-compose.yml",
|
||||
ROOT / "deployment/docker-compose.jobs.yml",
|
||||
)
|
||||
combined = "\n".join(path.read_text(encoding="utf-8") for path in compose_paths)
|
||||
self.assertNotIn("\n build:", combined)
|
||||
for variable in image_variables:
|
||||
self.assertIn(f"${{{variable}:?", combined, variable)
|
||||
|
||||
example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
||||
for variable in image_variables:
|
||||
match = re.search(rf"^{variable}=(.+)$", example, re.MULTILINE)
|
||||
self.assertIsNotNone(match, variable)
|
||||
self.assertRegex(match.group(1), r"@sha256:[0-9a-f]{64}$")
|
||||
|
||||
preflight = (ROOT / "deployment/preflight.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("config --images", preflight)
|
||||
self.assertIn("config --services", preflight)
|
||||
self.assertIn("only nginx may publish production host ports", preflight)
|
||||
self.assertIn("still contains the example image/digest", preflight)
|
||||
self.assertIn("socket.getaddrinfo", preflight)
|
||||
self.assertIn('ipaddress.ip_network("10.0.0.0/8")', preflight)
|
||||
self.assertIn("hostname must resolve only to private VPC addresses", preflight)
|
||||
self.assertIn('--runtime-manifest "$MANIFEST"', preflight)
|
||||
for variable in image_variables:
|
||||
self.assertIn(variable, preflight)
|
||||
|
||||
def test_legacy_compose_tls_is_removed(self) -> None:
|
||||
root = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
nginx = (ROOT / "nginx/docker-compose.yml").read_text(encoding="utf-8")
|
||||
combined = f"{root}\n{nginx}"
|
||||
self.assertNotIn("nginx-certs", combined)
|
||||
self.assertNotIn("nginx-acme", combined)
|
||||
self.assertNotIn("\n certbot:", nginx)
|
||||
self.assertIn("/var/lib/han-chat/public-tls:/run/tls:ro", nginx)
|
||||
self.assertIn("/var/lib/han-chat/acme:/var/www/certbot:ro", nginx)
|
||||
|
||||
example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
||||
self.assertIn("NGINX_TLS_CERTIFICATE=/run/tls/fullchain.pem", example)
|
||||
self.assertIn("NGINX_TLS_CERTIFICATE_KEY=/run/tls/privkey.pem", example)
|
||||
|
||||
def test_runtime_services_have_compose_hardening(self) -> None:
|
||||
application = (ROOT / "infra/compose/application.yml").read_text(encoding="utf-8")
|
||||
for anchor in ("x-api-runtime: &api-runtime", "x-sms-runtime: &sms-runtime"):
|
||||
start = application.index(anchor)
|
||||
end = application.index("\n\n", start)
|
||||
runtime = application[start:end]
|
||||
for setting in (
|
||||
"read_only: true",
|
||||
'cap_drop: ["ALL"]',
|
||||
'security_opt: ["no-new-privileges:true"]',
|
||||
"/tmp:size=",
|
||||
"pids_limit:",
|
||||
"mem_limit:",
|
||||
"cpus:",
|
||||
):
|
||||
self.assertIn(setting, runtime, f"{anchor}: {setting}")
|
||||
|
||||
bitrix = application[
|
||||
application.index("\n bitrix-local-app:") : application.index("\nsecrets:")
|
||||
]
|
||||
for setting in (
|
||||
"read_only: true",
|
||||
'cap_drop: ["ALL"]',
|
||||
'security_opt: ["no-new-privileges:true"]',
|
||||
"/tmp:size=",
|
||||
"pids_limit:",
|
||||
"mem_limit:",
|
||||
"cpus:",
|
||||
):
|
||||
self.assertIn(setting, bitrix)
|
||||
|
||||
keycloak = application[
|
||||
application.index("\n keycloak:") : application.index("\n sms-service:")
|
||||
]
|
||||
self.assertNotIn("\n read_only:", keycloak)
|
||||
self.assertIn('cap_drop: ["ALL"]', keycloak)
|
||||
self.assertIn("pids_limit:", keycloak)
|
||||
self.assertIn("read_only is intentionally omitted", keycloak)
|
||||
|
||||
jobs = (ROOT / "deployment/docker-compose.jobs.yml").read_text(encoding="utf-8")
|
||||
self.assertEqual(jobs.count("<<: *python-job-runtime"), 4)
|
||||
for setting in ("read_only: true", 'cap_drop: ["ALL"]', "/tmp:size="):
|
||||
self.assertIn(setting, jobs)
|
||||
|
||||
def test_vm_and_nginx_security_defaults(self) -> None:
|
||||
setup = (ROOT / "deployment/scripts/setup-vm.sh").read_text(encoding="utf-8")
|
||||
env_example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
||||
@@ -87,7 +227,7 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
ROOT / "deployment/scripts/ssl-renew.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn("LOCK_ACCOUNT_PASSWORDS=true", setup)
|
||||
self.assertIn('LOCK_ACCOUNT_PASSWORDS="${LOCK_ACCOUNT_PASSWORDS:-true}"', setup)
|
||||
self.assertIn('passwd --lock root', setup)
|
||||
self.assertIn('passwd --lock "$DEPLOY_USER"', setup)
|
||||
self.assertIn("X11Forwarding no", setup)
|
||||
@@ -163,7 +303,8 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
self.assertIn("save 900 1", config)
|
||||
self.assertIn("user default off", acl)
|
||||
self.assertIn("~han:api:*", acl)
|
||||
self.assertIn("~han:safety:*", acl)
|
||||
self.assertNotIn("message_safety", acl)
|
||||
self.assertNotIn("REDIS_SAFETY_PASSWORD", acl)
|
||||
|
||||
def test_otel_has_redaction_and_persistent_queue(self) -> None:
|
||||
config = (ROOT / "observability/otel-collector.yaml").read_text(encoding="utf-8")
|
||||
@@ -223,7 +364,7 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
|
||||
application = (ROOT / "infra/compose/application.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("\n frontend-static:", application)
|
||||
self.assertIn("frontend-test-site", application)
|
||||
self.assertIn("${FRONTEND_STATIC_IMAGE:?", application)
|
||||
self.assertIn("frontend-static:/output", application)
|
||||
for service, command in (
|
||||
("sms-worker:", "han-sms-worker"),
|
||||
@@ -246,6 +387,9 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
def test_expo_public_environment_uses_static_property_access(self) -> None:
|
||||
config = (ROOT / "frontend-test-site/src/config.ts").read_text(encoding="utf-8")
|
||||
auth = (ROOT / "frontend-test-site/src/auth.ts").read_text(encoding="utf-8")
|
||||
dockerfile = (ROOT / "frontend-test-site/Dockerfile").read_text(encoding="utf-8")
|
||||
realm = (ROOT / "keycloak/realm/han-chat-realm.json").read_text(encoding="utf-8")
|
||||
application = (ROOT / "infra/compose/application.yml").read_text(encoding="utf-8")
|
||||
callback = (ROOT / "frontend-test-site/app/auth/callback.tsx").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
@@ -254,6 +398,10 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
self.assertIn("process.env.EXPO_PUBLIC_AUTH_BASE_URL", config)
|
||||
self.assertIn('path: "auth/callback"', auth)
|
||||
self.assertIn('window.location.assign(url)', auth)
|
||||
self.assertIn('test -n "$EXPO_PUBLIC_AUTH_BASE_URL"', dockerfile)
|
||||
self.assertIn("${PUBLIC_WEB_URL}/auth/callback", realm)
|
||||
self.assertNotIn("chat.han0107.ru", realm)
|
||||
self.assertIn("PUBLIC_WEB_URL: ${PUBLIC_WEB_URL:?", application)
|
||||
self.assertIn("completionStarted.current", callback)
|
||||
self.assertTrue((ROOT / "frontend-test-site/app/auth/callback.tsx").is_file())
|
||||
|
||||
@@ -261,7 +409,6 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
for relative_path in (
|
||||
"api-backend/alembic/env.py",
|
||||
"bitrix-local-app/alembic/env.py",
|
||||
"bitrix-sync/alembic/env.py",
|
||||
"sms-service/migrations/env.py",
|
||||
):
|
||||
env_script = (ROOT / relative_path).read_text(encoding="utf-8")
|
||||
@@ -295,7 +442,7 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
dedup_fix,
|
||||
)
|
||||
self.assertIn('down_revision: str | None = "0009_chat_message_max"', dedup_fix)
|
||||
self.assertIn('EXPECTED_API_DB_REVISION = "0010_contact_map_dedup"', main)
|
||||
self.assertIn('EXPECTED_API_DB_REVISION = "0012_safety_v2_checkpoint"', main)
|
||||
|
||||
def test_consent_audit_migration_supports_existing_and_fresh_databases(self) -> None:
|
||||
migration = (
|
||||
@@ -370,14 +517,41 @@ class InfrastructureConfigTests(unittest.TestCase):
|
||||
|
||||
for service in (
|
||||
"api-backend",
|
||||
"sms-service",
|
||||
"message-safety",
|
||||
"bitrix-local-app",
|
||||
"bitrix-sync",
|
||||
"keycloak",
|
||||
"nginx",
|
||||
"redis",
|
||||
"sms-service",
|
||||
):
|
||||
dockerfile = (ROOT / service / "Dockerfile").read_text(encoding="utf-8")
|
||||
self.assertIn("han-container-entrypoint", dockerfile, service)
|
||||
self.assertIn("sed -i 's/\\r$//'", dockerfile, service)
|
||||
self.assertIn("/bin/sh -n", dockerfile, service)
|
||||
|
||||
frontend = (ROOT / "frontend-test-site/Dockerfile").read_text(encoding="utf-8")
|
||||
self.assertIn('ENTRYPOINT ["/bin/sh", "-ec"]', frontend)
|
||||
self.assertNotIn("entrypoint.sh", frontend)
|
||||
|
||||
def test_legacy_stubs_are_absent_and_safety_is_remote_tls(self) -> None:
|
||||
for stub in ("message-safety", "bitrix-sync"):
|
||||
self.assertFalse((ROOT / stub / "pyproject.toml").exists())
|
||||
self.assertFalse((ROOT / stub / "Dockerfile").exists())
|
||||
self.assertFalse((ROOT / stub / "app/main.py").exists())
|
||||
|
||||
example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
||||
application = (ROOT / "infra/compose/application.yml").read_text(encoding="utf-8")
|
||||
jobs = (ROOT / "deployment/docker-compose.jobs.yml").read_text(encoding="utf-8")
|
||||
nginx = (ROOT / "nginx/nginx.conf.template").read_text(encoding="utf-8")
|
||||
for config in (example, application, jobs):
|
||||
self.assertIn("https://processing.internal:8443", config)
|
||||
self.assertIn("/internal/safety/v2", config)
|
||||
self.assertIn(
|
||||
"MESSAGE_SAFETY_EXTRA_HOST=processing.internal=192.168.0.4", example
|
||||
)
|
||||
self.assertIn("${MESSAGE_SAFETY_EXTRA_HOST:?", application)
|
||||
self.assertIn("MESSAGE_SAFETY_CA_HOST_PATH", example)
|
||||
self.assertNotIn("message-safety:", application)
|
||||
self.assertNotIn("bitrix-sync:", application)
|
||||
self.assertNotIn("bitrix_sync_upstream", nginx)
|
||||
|
||||
def test_env_validator_accepts_materialized_example(self) -> None:
|
||||
example = (ROOT / ".env.example").read_text(encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user