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.
96 lines
4.2 KiB
Python
96 lines
4.2 KiB
Python
import asyncio
|
|
from urllib.parse import quote, urlsplit
|
|
from uuid import uuid4
|
|
|
|
from fastapi.testclient import TestClient
|
|
from pwdlib import PasswordHash
|
|
from sqlalchemy import delete
|
|
|
|
from ai_platform.bootstrap.app import create_app
|
|
from ai_platform.bootstrap.wiring import build_app_credential_use_cases
|
|
from ai_platform.infrastructure.config.settings import Settings
|
|
from ai_platform.infrastructure.persistence.database import make_session_factory
|
|
from ai_platform.infrastructure.persistence.models import (
|
|
AdminSession,
|
|
AdminUser,
|
|
AppCredential,
|
|
)
|
|
from ai_platform.platform.security import digest
|
|
|
|
|
|
def test_operator_lifecycle_and_admin_session_revoke():
|
|
settings = Settings.load()
|
|
if urlsplit(settings.database_url).path.rstrip("/").rsplit("/", 1)[-1] != "ai_platform_test":
|
|
raise RuntimeError("App API tests require /ai_platform_test")
|
|
sessions = make_session_factory(settings.database_url)
|
|
app_id = f"omni/{uuid4().hex}"
|
|
email, password = f"app-admin-{uuid4().hex}@example.invalid", "test-only-password"
|
|
path = f"/v1/admin/apps/{quote(app_id, safe='')}"
|
|
|
|
async def setup():
|
|
async with sessions() as db:
|
|
db.add(AdminUser(email=email, password_hash=PasswordHash.recommended().hash(password)))
|
|
await db.commit()
|
|
key = await build_app_credential_use_cases(db).create(app_id)
|
|
await sessions.kw["bind"].dispose()
|
|
return key
|
|
|
|
async def rotate():
|
|
async with sessions() as db:
|
|
key = await build_app_credential_use_cases(db).rotate(app_id)
|
|
await sessions.kw["bind"].dispose()
|
|
return key
|
|
|
|
async def cleanup():
|
|
async with sessions() as db:
|
|
await db.execute(delete(AppCredential).where(AppCredential.app_id == app_id))
|
|
await db.execute(delete(AdminSession).where(AdminSession.admin_email == email))
|
|
await db.execute(delete(AdminUser).where(AdminUser.email == email))
|
|
await db.commit()
|
|
await sessions.kw["bind"].dispose()
|
|
|
|
initial_key = asyncio.run(setup())
|
|
try:
|
|
with TestClient(create_app(settings)) as client:
|
|
assert client.get("/v1/admin/apps").status_code == 401
|
|
assert client.delete(path).status_code == 401
|
|
assert client.post("/v1/platform/tenant-keys", json={}).status_code == 404
|
|
assert client.post("/v1/admin/apps", json={"app_id": "new"}).status_code == 405
|
|
login = client.post("/v1/admin/login", json={"email": email, "password": password})
|
|
assert login.status_code == 200
|
|
csrf = {"x-csrf-token": login.json()["csrf_token"]}
|
|
listed = client.get("/v1/admin/apps")
|
|
assert listed.status_code == 200
|
|
item = next(x for x in listed.json()["apps"] if x["app_id"] == app_id)
|
|
assert set(item) == {"app_id", "active", "created_at", "updated_at"}
|
|
assert item["active"]
|
|
assert initial_key not in listed.text and "verifier" not in listed.text
|
|
assert client.delete(path).status_code == 403
|
|
|
|
def probe(key: str) -> int:
|
|
# An empty body is 422 once authenticated, 401 before.
|
|
headers = {"Authorization": f"Bearer {key}"}
|
|
return client.post("/v1/lead-scoring/score", headers=headers, json={}).status_code
|
|
|
|
assert probe(initial_key) == 422
|
|
rotated_key = asyncio.run(rotate())
|
|
assert rotated_key != initial_key
|
|
assert probe(initial_key) == 401
|
|
assert probe(rotated_key) == 422
|
|
revoked = client.delete(path, headers=csrf)
|
|
assert revoked.status_code == 200 and revoked.json()["active"] is False
|
|
assert probe(rotated_key) == 401
|
|
active_key = asyncio.run(rotate())
|
|
assert probe(active_key) == 422
|
|
assert active_key not in client.get("/v1/admin/apps").text
|
|
|
|
async def verify():
|
|
async with sessions() as db:
|
|
row = await db.get(AppCredential, app_id)
|
|
assert row is not None and row.active and row.verifier == digest(active_key)
|
|
await sessions.kw["bind"].dispose()
|
|
|
|
asyncio.run(verify())
|
|
finally:
|
|
asyncio.run(cleanup())
|