from dataclasses import replace from datetime import UTC, datetime import pytest from ai_platform.platform.application.app_credentials import ( AppCredentialConflict, AppCredentialNotFound, AppCredentialRecord, AppCredentialUseCases, AppFeatureForbidden, ) from ai_platform.platform.security import digest class Repository: def __init__(self): self.record = AppCredentialRecord( "omni-local", True, True, datetime.now(UTC), datetime.now(UTC) ) self.verifier = digest("initial-key") self.events = [] self.error: Exception | None = None async def list(self): return [self.record] async def active_app_for_verifier(self, verifier): return self.record if self.record.active and verifier == self.verifier else None async def create(self, app_id, verifier, lead_scoring, actor): if self.error: raise self.error self.record = replace(self.record, app_id=app_id, lead_scoring=lead_scoring) self.verifier = verifier self.events.append(("create", actor)) async def rotate(self, app_id, verifier, actor): if self.error: raise self.error self.verifier = verifier self.record = replace(self.record, active=True) self.events.append(("rotate", actor)) async def revoke(self, app_id, actor): if self.error: raise self.error self.record = replace(self.record, active=False) self.events.append(("revoke", actor)) return self.record class Transaction: def __init__(self): self.commits = 0 self.rollbacks = 0 self.error: Exception | None = None async def commit(self): self.commits += 1 if self.error: raise self.error async def rollback(self): self.rollbacks += 1 async def test_lifecycle_preserves_identity_and_stores_only_verifier(): repo, tx = Repository(), Transaction() keys = iter(["created-key", "rotated-key", "reactivated-key"]) cases = AppCredentialUseCases(repo, tx.commit, tx.rollback, lambda: next(keys)) assert await cases.create("omni-local", "operator") == "created-key" assert repo.verifier == digest("created-key") assert await cases.authenticate("created-key") == "omni-local" assert await cases.rotate("omni-local", "operator") == "rotated-key" assert await cases.authenticate("created-key") is None assert await cases.authenticate("rotated-key") == "omni-local" assert not (await cases.revoke("omni-local", "operator")).active assert await cases.authenticate("rotated-key") is None await cases.rotate("omni-local", "operator") assert await cases.authenticate("reactivated-key") == "omni-local" assert tx.commits == 4 and tx.rollbacks == 0 assert repo.events == [(name, "operator") for name in ("create", "rotate", "revoke", "rotate")] async def test_valid_app_without_feature_permission_is_forbidden(): repo, tx = Repository(), Transaction() repo.record = replace(repo.record, lead_scoring=False) cases = AppCredentialUseCases(repo, tx.commit, tx.rollback, lambda: "key") with pytest.raises(AppFeatureForbidden): await cases.authenticate("initial-key") assert await cases.authenticate("incorrect-key") is None await cases.rotate("omni-local", "operator") assert repo.record.lead_scoring is False assert await cases.list_apps() == [repo.record] @pytest.mark.parametrize("operation", ["create", "rotate", "revoke"]) @pytest.mark.parametrize( "error", [AppCredentialConflict(), AppCredentialNotFound(), RuntimeError("storage")] ) async def test_repository_error_rolls_back(operation, error): repo, tx = Repository(), Transaction() repo.error = error cases = AppCredentialUseCases(repo, tx.commit, tx.rollback, lambda: "key") with pytest.raises(type(error)): await getattr(cases, operation)("omni-local", "operator") assert tx.commits == 0 and tx.rollbacks == 1 async def test_failed_commit_rolls_back(): repo, tx = Repository(), Transaction() tx.error = RuntimeError("commit failed") cases = AppCredentialUseCases(repo, tx.commit, tx.rollback, lambda: "key") with pytest.raises(RuntimeError, match="commit failed"): await cases.rotate("omni-local", "operator") assert tx.commits == 1 and tx.rollbacks == 1