dotb_ai-platform/tests/test_app_admin_api.py
hungtran24 b1f6643796 refactor: score synchronously and drop webhook/background execution
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.
2026-10-09 02:44:24 +07:00

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())