Files
LegacyHUB/tests/test_alembic.py
Vadim Malanov d27dd0ffbb feat: align LegacyHUB with TeamHUB platform contract (D2/D4, assets, security)
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>
2026-06-15 11:44:15 +03:00

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"]