102 lines
4.8 KiB
Python
102 lines
4.8 KiB
Python
# ruff: noqa: S106 - isolated synthetic persistence fixtures
|
|
"""Secret history fingerprint and migration contracts without external credentials."""
|
|
import copy
|
|
import importlib.util
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from alembic.migration import MigrationContext
|
|
from alembic.operations import Operations
|
|
from sqlalchemy import create_engine, select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from modelforge_api.persistence.models import Base, SecretSlotState, SecretSlotVersion
|
|
from modelforge_api.services.audit import AuditWriter
|
|
from modelforge_api.services.recovery_fingerprint import (
|
|
SECRET_SCHEMA,
|
|
SECRET_TABLES,
|
|
FingerprintCompatibility,
|
|
_without_creative,
|
|
_without_lab_execution,
|
|
_without_model_changes,
|
|
_without_model_updates,
|
|
_without_secrets,
|
|
_without_selected_comparisons,
|
|
audit_fingerprint_differences_are_compatible,
|
|
fingerprint_compatibility,
|
|
unclassified_tables,
|
|
)
|
|
from modelforge_api.services.recovery_fingerprint import (
|
|
fingerprint_session as _current_fingerprint_session,
|
|
)
|
|
|
|
|
|
def fingerprint_session(session: Session):
|
|
return _without_lab_execution(_without_model_changes(_current_fingerprint_session(session)))
|
|
|
|
|
|
def test_secret_history_is_strict_except_verified_recovery_marker() -> None:
|
|
engine = create_engine("sqlite://")
|
|
Base.metadata.create_all(engine)
|
|
with Session(engine) as session:
|
|
assert not unclassified_tables()
|
|
session.add(SecretSlotVersion(version=1, key_id="qualification", secret_ciphertext="opaque",
|
|
secret_nonce="nonce", created_at=datetime.now(UTC)))
|
|
session.commit()
|
|
before = _without_creative(_without_model_updates(_without_selected_comparisons(fingerprint_session(session))))
|
|
assert before["tables"]["platform_secret_versions"]["redacted_columns"] == ["secret_ciphertext", "secret_nonce"]
|
|
AuditWriter(session, "operator", "qualification").write("RECOVERY_RECONCILIATION_COMPLETED", "restore", None, {"restored": True})
|
|
session.commit()
|
|
after = _without_creative(_without_model_updates(_without_selected_comparisons(fingerprint_session(session))))
|
|
contract = fingerprint_compatibility(before, after, source_schema_revision=SECRET_SCHEMA)
|
|
assert contract.mode == "ALERT_NOTIFICATION_UPGRADE|PORTFOLIO_UPGRADE|SECRET_STRICT"
|
|
assert audit_fingerprint_differences_are_compatible(before, after, contract)
|
|
unrelated_marker = FingerprintCompatibility(
|
|
True,
|
|
f"UNRELATED|{contract.mode}",
|
|
contract.allow_audit_checkpoint_introduction,
|
|
contract.reason,
|
|
)
|
|
assert not audit_fingerprint_differences_are_compatible(before, after, unrelated_marker)
|
|
for table in SECRET_TABLES:
|
|
altered = copy.deepcopy(after)
|
|
altered["tables"][table]["digest"] = "0" * 64
|
|
assert not audit_fingerprint_differences_are_compatible(before, altered, contract)
|
|
engine.dispose()
|
|
|
|
|
|
def test_prior_schema_requires_exact_empty_secret_introduction() -> None:
|
|
engine = create_engine("sqlite://")
|
|
Base.metadata.create_all(engine)
|
|
with Session(engine) as session:
|
|
after = _without_creative(_without_model_updates(_without_selected_comparisons(fingerprint_session(session))))
|
|
before = _without_secrets(after)
|
|
contract = fingerprint_compatibility(before, after, source_schema_revision="20260908_0027")
|
|
assert contract.compatible
|
|
assert audit_fingerprint_differences_are_compatible(before, after, contract)
|
|
for table in SECRET_TABLES:
|
|
altered = copy.deepcopy(after)
|
|
altered["tables"][table]["digest"] = "0" * 64
|
|
assert not fingerprint_compatibility(before, altered, source_schema_revision="20260908_0027").compatible
|
|
engine.dispose()
|
|
|
|
|
|
def test_secret_migration_seed_empty_downgrade_and_history_refusal() -> None:
|
|
path = Path(__file__).resolve().parents[1] / "alembic/versions/20260908_0028_platform_secret_slots.py"
|
|
spec = importlib.util.spec_from_file_location("secret_migration", path)
|
|
assert spec and spec.loader
|
|
migration = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(migration)
|
|
engine = create_engine("sqlite://")
|
|
with engine.begin() as connection, Operations.context(MigrationContext.configure(connection)):
|
|
migration.upgrade()
|
|
assert connection.scalar(select(SecretSlotState.revision)) == 0
|
|
migration.downgrade()
|
|
migration.upgrade()
|
|
connection.execute(SecretSlotVersion.__table__.insert().values(version=1, key_id="qualification",
|
|
secret_ciphertext="opaque", secret_nonce="nonce", created_at=datetime.now(UTC)))
|
|
with pytest.raises(RuntimeError, match="history exists"):
|
|
migration.downgrade()
|
|
engine.dispose()
|