feat: prepare postgres sbom cutover

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a028f0-a42f-7582-89a8-ebaad7343834
This commit is contained in:
tegwick 2026-08-22 13:14:24 +02:00
parent cf7e3acb78
commit ba535e1f8f
26 changed files with 1573 additions and 411 deletions

View file

@ -33,7 +33,11 @@ def register(
def test_health_and_legacy_ingest_query_and_licence_report(tmp_path: Path) -> None:
client = client_for(tmp_path)
assert client.get("/state/health").json() == {"status": "ok", "store": "connected"}
assert client.get("/state/health").json() == {
"status": "ok",
"store": "connected",
"dialect": "sqlite",
}
register(client, "demo")
response = client.post(

View file

@ -0,0 +1,103 @@
from __future__ import annotations
from typing import Any
from sbom_nexus import importer
def test_import_reconciles_history_licences_and_is_idempotent(monkeypatch) -> None:
target_snapshots: list[dict[str, Any]] = []
groups = [
{
"license_spdx": "MIT",
"count": 1,
"repos": ["demo"],
"is_copyleft": False,
},
{
"license_spdx": None,
"count": 2,
"repos": ["zeta", "demo"],
"is_copyleft": False,
},
]
def fake_request(
base_url: str,
path: str,
*,
method: str = "GET",
body: dict[str, Any] | None = None,
) -> Any:
if base_url == "source":
if path == "/repos/":
return [
{
"id": "repo-id",
"slug": "demo",
"status": "active",
"local_path": "/repos/demo",
}
]
if path == "/sbom/snapshots/":
return [
{
"id": "snapshot-id",
"repo_id": "repo-id",
"snapshot_at": "2026-01-02T03:04:05Z",
"source": "manual",
"entry_count": 1,
}
]
if path == "/sbom/snapshots/snapshot-id":
return {
"entries": [
{
"package_name": "example",
"package_version": "1.0",
"ecosystem": "other",
"license_spdx": "MIT",
"is_direct": True,
"is_dev": False,
}
]
}
if path == "/sbom/report/licences/":
return {"groups": list(reversed(groups)), "copyleft_direct_count": 0}
if base_url == "target":
if method == "PUT":
return {"slug": "demo"}
if method == "POST" and path == "/sbom/import/":
assert body is not None
already_present = any(
snapshot["legacy_id"] == body["legacy_id"]
for snapshot in target_snapshots
)
if not already_present:
target_snapshots.append(
{
"legacy_id": body["legacy_id"],
"repo_slug": body["repo_slug"],
"snapshot_at": body["snapshot_at"],
"entry_count": len(body["entries"]),
}
)
return {"imported": not already_present}
if path == "/sbom/snapshots/":
return target_snapshots
if path == "/sbom/report/licences/":
return {"groups": groups, "copyleft_direct_count": 0}
raise AssertionError((base_url, method, path))
monkeypatch.setattr(importer, "request_json", fake_request)
first = importer.import_history("source", "target", dry_run=False)
second = importer.import_history("source", "target", dry_run=False)
assert first["ok"] is True
assert first["results"] == {"imported": 1}
assert first["snapshot_reconciliation"]["matched_snapshot_count"] == 1
assert first["licence_reconciliation"]["groups_match"] is True
assert second["ok"] is True
assert second["results"] == {"already_present": 1}
assert len(target_snapshots) == 1

38
tests/test_migrations.py Normal file
View file

@ -0,0 +1,38 @@
from __future__ import annotations
from pathlib import Path
from alembic import command
from alembic.config import Config
from sqlalchemy import create_engine, inspect
def migration_config(database_path: Path) -> Config:
config = Config("alembic.ini")
config.set_main_option("sqlalchemy.url", f"sqlite:///{database_path}")
return config
def test_initial_migration_upgrades_and_downgrades(tmp_path: Path) -> None:
database_path = tmp_path / "migrated.db"
config = migration_config(database_path)
command.upgrade(config, "head")
engine = create_engine(f"sqlite:///{database_path}")
assert set(inspect(engine).get_table_names()) == {
"alembic_version",
"entries",
"repositories",
"snapshots",
}
assert {index["name"] for index in inspect(engine).get_indexes("entries")} == {
"ix_entries_license",
"ix_entries_repo",
"ix_entries_snapshot",
}
command.check(config)
command.downgrade(config, "base")
assert set(inspect(engine).get_table_names()) == {"alembic_version"}

View file

@ -0,0 +1,52 @@
from __future__ import annotations
import os
import pytest
from fastapi.testclient import TestClient
from sbom_nexus.api import create_app
POSTGRES_URL = os.getenv("SBOM_NEXUS_TEST_POSTGRES_URL")
pytestmark = pytest.mark.skipif(
not POSTGRES_URL,
reason="SBOM_NEXUS_TEST_POSTGRES_URL is not configured",
)
def test_migrated_postgres_runtime_contract() -> None:
assert POSTGRES_URL is not None
client = TestClient(create_app(POSTGRES_URL))
assert client.get("/state/health").json() == {
"status": "ok",
"store": "connected",
"dialect": "postgresql",
}
response = client.put(
"/repositories/postgres-contract",
json={"active": True},
)
assert response.status_code == 200
ingest = client.post(
"/sbom/ingest/",
json={
"repo_slug": "postgres-contract",
"entries": [
{
"package_name": "psycopg",
"package_version": "3.2",
"ecosystem": "python",
"license_spdx": "LGPL-3.0-only",
"is_direct": True,
"is_dev": False,
}
],
},
)
assert ingest.status_code == 200
assert ingest.json()["ingested"] == 1
assert client.get("/sbom/postgres-contract").json()["entry_count"] == 1
assert client.get("/sbom/report/licences/").json()["copyleft_direct_count"] == 1