Close 12 audit-driven platform-compliance gaps on a single branch. - D4 dispatch: app/integrations/dispatch_client.py participant `legacyhub`, emits LegacyhubDocumentIndexed + AssetDerivativeReady after the indexing commit (idempotent uuid5), http_inbox route (reindex/tombstone) with audit-based dedupe; docs/dispatch-contract.md. Celery+Redis stays intra-module. - D2 SSO: app/integrations/identity.py validates X-TeamHub-* + role/scope mapper; security.py adds trusted-header enforcement (AUTH_REQUIRE_IDENTITY) and a scope check on /search; docker-compose.teamhub.yml (external teamhub_net + internal db net, api not host-published); RUNBOOK network/firewall section. - Asset standard: SearchHit/Citation carry asset_id/owner_module; buckets renamed teamhub-legacyhub-* (+quarantine/tmp/exports); purge-by-asset_id with legal-hold guard (app/indexing/projection.py); OCR-markdown derivative event. - audit_log model + Alembic 0003 + record_audit on writes (same transaction). - Secret masking: app/common/json_logger.py recursive mask wired into structlog (+ensure_ascii=False); event payloads redacted before persistence. - Service X-API-Key mandatory on ingest endpoints (defence-in-depth). - Port: host API 8000->8050 (collision with SalesHUB/MailHUB resolved), container still listens on 8000. - Config: no plaintext secret defaults; fail-loud in non-dev (no value leak). - Docs drift: README PG 5440, layered-auth note, 5173 removed from CORS; ingest/folder gated by ENABLE_FOLDER_INGEST (410 by default). - ADRs: layers mapping, shared-core extraction, UI locale (RU-first). Tests: 78 passing (ruff, compileall, pytest, tsc, vite build, compose config). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
2.7 KiB
Python
76 lines
2.7 KiB
Python
"""Alembic migration smoke test.
|
|
|
|
We do not boot a real Postgres in CI; instead we point Alembic at an in-process
|
|
SQLite database and verify:
|
|
|
|
- ``alembic upgrade head`` succeeds offline (SQL generation) using the real
|
|
migration files, exercising every column type and constraint declaration;
|
|
- ``downgrade base`` rewinds without errors.
|
|
|
|
This catches typos and broken migration ordering early without requiring the
|
|
full backing-service compose stack to be online.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from alembic import command
|
|
from alembic.config import Config
|
|
from alembic.script import ScriptDirectory
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
@pytest.fixture
|
|
def alembic_cfg(tmp_path, monkeypatch):
|
|
"""Configure Alembic against an isolated SQLite file."""
|
|
db_file = tmp_path / "legacyhub.db"
|
|
monkeypatch.setenv("POSTGRES_HOST", "127.0.0.1")
|
|
monkeypatch.setenv("POSTGRES_PORT", "5432")
|
|
cfg = Config(str(ROOT / "alembic.ini"))
|
|
cfg.set_main_option("script_location", str(ROOT / "app" / "db" / "migrations"))
|
|
cfg.set_main_option("sqlalchemy.url", f"sqlite:///{db_file}")
|
|
return cfg
|
|
|
|
|
|
def test_migration_offline_emits_sql(alembic_cfg, tmp_path):
|
|
"""Offline mode generates SQL for every table; verify ``documents`` appears
|
|
and the TeamHUB AssetManifest ingest job table is part of the linear head.
|
|
SQLite has no JSONB but Alembic's offline mode happily emits the raw DDL for
|
|
inspection.
|
|
"""
|
|
out_file = tmp_path / "upgrade.sql"
|
|
# ``--sql`` mode bypasses dialect-specific runtime, perfect for a fast check.
|
|
with out_file.open("w", encoding="utf-8") as f:
|
|
old_stdout = sys.stdout
|
|
sys.stdout = f
|
|
try:
|
|
command.upgrade(alembic_cfg, "head", sql=True)
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
sql = out_file.read_text(encoding="utf-8")
|
|
assert "CREATE TABLE documents" in sql
|
|
assert "CREATE TABLE chunks" in sql
|
|
assert "CREATE TABLE processing_events" in sql
|
|
assert "CREATE TABLE asset_ingest_jobs" in sql
|
|
assert "CREATE TABLE audit_log" in sql
|
|
# Constraint sanity
|
|
assert "uq_chunks_doc_idx" in sql
|
|
assert "uq_pages_doc_page" in sql
|
|
assert "uq_asset_ingest_identity" in sql
|
|
|
|
|
|
def test_revision_history_is_linear(alembic_cfg):
|
|
"""The project keeps a single linear migration head."""
|
|
script = ScriptDirectory.from_config(alembic_cfg)
|
|
heads = script.get_heads()
|
|
assert len(heads) == 1, f"expected one head, got: {heads}"
|
|
assert heads == ["0003_audit_log"]
|
|
revisions = [rev.revision for rev in script.walk_revisions()]
|
|
assert revisions == ["0003_audit_log", "0002_asset_ingest_jobs", "0001_initial"]
|