POST /v1/lead-scoring/score now evaluates in the request, applies an overall AI deadline, and stores the completed run only after scoring. Idempotency is kept by (app_id, account_id, request_id) with a body fingerprint. Removes the webhook outbox, scheduler/recovery loop, app lead_scoring permission, credential audit events, seed_demo and generate_secret scripts, and the tests that covered them. Trims tests to behavior of the new flow.
98 lines
3.3 KiB
Python
98 lines
3.3 KiB
Python
from dataclasses import replace
|
|
from datetime import UTC, datetime
|
|
|
|
import pytest
|
|
|
|
from ai_platform.platform.application.app_credentials import (
|
|
AppCredentialConflict,
|
|
AppCredentialNotFound,
|
|
AppCredentialRecord,
|
|
AppCredentialUseCases,
|
|
)
|
|
from ai_platform.platform.security import digest
|
|
|
|
|
|
class Repository:
|
|
def __init__(self):
|
|
self.record = AppCredentialRecord("omni-local", True, datetime.now(UTC), datetime.now(UTC))
|
|
self.verifier = digest("initial-key")
|
|
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):
|
|
if self.error:
|
|
raise self.error
|
|
self.record = replace(self.record, app_id=app_id)
|
|
self.verifier = verifier
|
|
|
|
async def rotate(self, app_id, verifier):
|
|
if self.error:
|
|
raise self.error
|
|
self.verifier = verifier
|
|
self.record = replace(self.record, active=True)
|
|
|
|
async def revoke(self, app_id):
|
|
if self.error:
|
|
raise self.error
|
|
self.record = replace(self.record, active=False)
|
|
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") == "created-key"
|
|
assert repo.verifier == digest("created-key")
|
|
assert await cases.authenticate("created-key") == "omni-local"
|
|
assert await cases.rotate("omni-local") == "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")).active
|
|
assert await cases.authenticate("rotated-key") is None
|
|
await cases.rotate("omni-local")
|
|
assert await cases.authenticate("reactivated-key") == "omni-local"
|
|
assert tx.commits == 4 and tx.rollbacks == 0
|
|
|
|
|
|
@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")
|
|
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")
|
|
assert tx.commits == 1 and tx.rollbacks == 1
|