"""Schema obligations from business-app-service-contract_v0.1 § 1.3.""" from __future__ import annotations from canned_prompts_service import models def test_every_owned_table_is_tenant_keyed() -> None: for table in models.Base.metadata.sorted_tables: assert "tenant" in table.c, f"{table.name} holds owned data without a tenant key" def test_uniqueness_is_scoped_by_tenant_and_registry() -> None: """Identity is registry-scoped (§ 3.2); two tenants may hold the same id.""" constraint = next( c for c in models.PackageVersion.__table__.constraints if getattr(c, "name", "") == "uq_package_version" ) assert [c.name for c in constraint.columns] == ["tenant", "registry", "package_id", "version"] def test_index_entry_records_arrival_not_authorship() -> None: columns = models.IndexEntry.__table__.c assert "included_at" in columns and "last_seen_at" in columns assert "source" in columns and "method" in columns # --- migration role assumption (rapp-postgres boundary requirement) --- import pytest from canned_prompts_service import db as service_db class FakeConnection: def __init__(self) -> None: self.statements: list[str] = [] self.commits = 0 def execute(self, statement) -> None: self.statements.append(str(statement)) def commit(self) -> None: self.commits += 1 def test_no_role_assumed_when_unset(monkeypatch) -> None: monkeypatch.delenv("CANNED_PROMPTS_MIGRATION_ROLE", raising=False) conn = FakeConnection() service_db.assume_owner_role(conn) assert conn.statements == [] def test_owner_role_is_assumed(monkeypatch) -> None: """Objects must end up owned by the durable role, not a leased login.""" monkeypatch.setenv("CANNED_PROMPTS_MIGRATION_ROLE", "canned_prompts_owner") conn = FakeConnection() service_db.assume_owner_role(conn) assert conn.statements == ['SET ROLE "canned_prompts_owner"'] @pytest.mark.parametrize("bad", ['x"; drop schema public; --', "role-with-dash", "1role", ""]) def test_role_name_is_validated_not_trusted(monkeypatch, bad) -> None: """SET ROLE cannot be parameterised, so the name is validated.""" monkeypatch.setenv("CANNED_PROMPTS_MIGRATION_ROLE", bad) conn = FakeConnection() if bad == "": service_db.assume_owner_role(conn) assert conn.statements == [] else: with pytest.raises(ValueError, match="invalid migration role"): service_db.assume_owner_role(conn) def test_configured_but_env_field_empty_is_the_cluster_case() -> None: """The bug that broke the first migration: `configured` was true because a file was set, while the plain field it then read was empty.""" from canned_prompts_service.settings import Settings import tempfile, os with tempfile.NamedTemporaryFile("w", suffix=".url", delete=False) as f: f.write("postgresql+psycopg://u:p%40x@h/db") path = f.name settings = Settings(database_url_file=path) assert settings.configured is True assert settings.database_url == "" assert settings.resolved_database_url.startswith("postgresql+psycopg://") # A percent in the password must survive ConfigParser interpolation. assert settings.resolved_database_url.replace("%", "%%").count("%%") == 1 os.unlink(path) def test_set_role_commits_so_alembic_owns_its_transaction(monkeypatch) -> None: """Regression: SET ROLE opened an implicit transaction that Alembic then nested inside rather than owning, so every migration was rolled back while logging as applied.""" monkeypatch.setenv("CANNED_PROMPTS_MIGRATION_ROLE", "canned_prompts_owner") conn = FakeConnection() service_db.assume_owner_role(conn) assert conn.commits == 1, "SET ROLE must not leave an open transaction" def test_no_commit_when_no_role_is_assumed(monkeypatch) -> None: monkeypatch.delenv("CANNED_PROMPTS_MIGRATION_ROLE", raising=False) conn = FakeConnection() service_db.assume_owner_role(conn) assert conn.commits == 0