dotb_ai-platform/tests/test_app_run_context_api.py
hungtran24 2268b6a77a feat: replace tenant auth with app-scoped scoring access
Add app credentials, account-scoped idempotency and per-app terminal notifications. Consolidate the unreleased migration baseline, preserve rule descriptions as AI context, and regenerate provider contracts with regression coverage.
2026-10-02 10:56:58 +07:00

224 lines
8.9 KiB
Python

import asyncio
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from uuid import uuid4
import httpx
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import delete, select
from ai_platform.infrastructure.persistence.models import AppCredential, ScoringRun
from ai_platform.platform.security import digest
def test_app_account_identity_replay_rotation_and_concurrency(
aihub_test_environment, valid_run_payload
):
env = aihub_test_environment
factory = env["factory"]
other_app, other_key = f"other-{uuid4().hex}", f"other-key-{uuid4().hex}"
env["settings"].app_webhooks[other_app] = env["settings"].app_webhooks[env["app_id"]]
async def extra_app():
async with factory() as db:
db.add(AppCredential(app_id=other_app, verifier=digest(other_key)))
await db.commit()
await factory.kw["bind"].dispose()
asyncio.run(extra_app())
app = env["app_for"](lambda _request: httpx.Response(500))
scheduled = []
auth = {
"Authorization": f"Bearer {env['app_key']}",
"Idempotency-Key": valid_run_payload["request_id"],
}
other_auth = {**auth, "Authorization": f"Bearer {other_key}"}
try:
with TestClient(app) as client:
app.state.schedule_scoring_run = lambda run_id, _key: scheduled.append(run_id)
first = client.post("/v1/lead-scoring/runs", headers=auth, json=valid_run_payload)
assert first.status_code == 202
first_body = first.json()
assert first.headers["Location"] == f"/v1/lead-scoring/runs/{first_body['run_id']}"
assert first_body["app_id"] == env["app_id"] and first_body["account_id"] == "3"
assert "tenant_id" not in first_body
replay = client.post(
"/v1/lead-scoring/runs",
headers=auth,
json=dict(reversed(list(valid_run_payload.items()))),
)
assert replay.status_code == 200 and replay.json()["run_id"] == first_body["run_id"]
changed = deepcopy(valid_run_payload)
changed["scoring_config"]["rules"][0]["points"] = 24
assert (
client.post("/v1/lead-scoring/runs", headers=auth, json=changed).status_code == 409
)
account_b = {**valid_run_payload, "account_id": "4"}
second = client.post("/v1/lead-scoring/runs", headers=auth, json=account_b)
third = client.post("/v1/lead-scoring/runs", headers=other_auth, json=valid_run_payload)
assert second.status_code == third.status_code == 202
assert len({first_body["run_id"], second.json()["run_id"], third.json()["run_id"]}) == 3
url = first.headers["Location"]
assert client.get(url, headers=other_auth).status_code == 404
detail = client.get(url, headers=auth).json()
assert (detail["app_id"], detail["account_id"], detail["request_id"]) == (
env["app_id"],
"3",
valid_run_payload["request_id"],
)
assert "tenant_id" not in detail
rotated_key = f"rotated-{uuid4().hex}"
async def rotate():
async with factory() as db:
row = await db.get(AppCredential, env["app_id"])
assert row is not None
row.verifier = digest(rotated_key)
await db.commit()
await factory.kw["bind"].dispose()
asyncio.run(rotate())
assert client.get(url, headers=auth).status_code == 401
new_auth = {**auth, "Authorization": f"Bearer {rotated_key}"}
assert client.get(url, headers=new_auth).status_code == 200
assert (
client.post(
"/v1/lead-scoring/runs", headers=new_auth, json=valid_run_payload
).json()["run_id"]
== first_body["run_id"]
)
concurrent = {**valid_run_payload, "request_id": str(uuid4())}
headers = {**new_auth, "Idempotency-Key": concurrent["request_id"]}
with ThreadPoolExecutor(max_workers=2) as pool:
responses = list(
pool.map(
lambda _: client.post(
"/v1/lead-scoring/runs", headers=headers, json=concurrent
),
range(2),
)
)
assert sorted(r.status_code for r in responses) == [200, 202]
assert responses[0].json()["run_id"] == responses[1].json()["run_id"]
assert len(scheduled) == 4
# Replay lookup precedes webhook availability, even after configuration removal.
env["settings"].app_webhooks.pop(env["app_id"])
assert (
client.post(
"/v1/lead-scoring/runs", headers=new_auth, json=valid_run_payload
).status_code
== 200
)
missing = {**valid_run_payload, "request_id": str(uuid4())}
assert (
client.post(
"/v1/lead-scoring/runs",
headers={**new_auth, "Idempotency-Key": missing["request_id"]},
json=missing,
).status_code
== 503
)
assert len(scheduled) == 4
async def verify():
async with factory() as db:
rows = (
await db.scalars(
select(ScoringRun).where(ScoringRun.app_id.in_([env["app_id"], other_app]))
)
).all()
assert len(rows) == 4
assert all(
env["app_key"] not in str(row.request) and rotated_key not in str(row.request)
for row in rows
)
await factory.kw["bind"].dispose()
asyncio.run(verify())
finally:
async def cleanup():
async with factory() as db:
await db.execute(delete(ScoringRun).where(ScoringRun.app_id == other_app))
await db.execute(delete(AppCredential).where(AppCredential.app_id == other_app))
await db.commit()
await factory.kw["bind"].dispose()
asyncio.run(cleanup())
@pytest.mark.parametrize("state,expected", [("inactive", 401), ("forbidden", 403)])
def test_permissions_apply_to_post_and_get(
aihub_test_environment, valid_run_payload, state, expected
):
env = aihub_test_environment
async def disable():
async with env["factory"]() as db:
row = await db.get(AppCredential, env["app_id"])
assert row is not None
if state == "inactive":
row.active = False
else:
row.lead_scoring = False
await db.commit()
await env["factory"].kw["bind"].dispose()
asyncio.run(disable())
with TestClient(env["app_for"](lambda _: httpx.Response(500))) as client:
headers = {
"Authorization": f"Bearer {env['app_key']}",
"Idempotency-Key": valid_run_payload["request_id"],
}
assert (
client.post(
"/v1/lead-scoring/runs", headers=headers, json=valid_run_payload
).status_code
== expected
)
assert (
client.get(f"/v1/lead-scoring/runs/{uuid4()}", headers=headers).status_code == expected
)
assert (
client.get(
f"/v1/lead-scoring/runs/{uuid4()}", headers={"Authorization": "Bearer wrong"}
).status_code
== 401
)
assert (
client.post(
"/v1/lead-scoring/runs",
headers={"Idempotency-Key": valid_run_payload["request_id"]},
json=valid_run_payload,
).status_code
== 401
)
def test_credentials_and_validation_errors_are_redacted(
aihub_test_environment, valid_run_payload, caplog
):
env = aihub_test_environment
app = env["app_for"](lambda _: httpx.Response(500))
with TestClient(app) as client:
headers = {
"Authorization": f"Bearer {env['app_key']}",
"Idempotency-Key": valid_run_payload["request_id"],
}
for field in ("app_id", "tenant_id", "callback_url"):
response = client.post(
"/v1/lead-scoring/runs",
headers=headers,
json={**valid_run_payload, field: env["app_key"]},
)
assert response.status_code == 422 and env["app_key"] not in response.text
embedded = deepcopy(valid_run_payload)
embedded["snapshot"]["lead_profile"]["name"] = env["app_key"]
assert (
client.post("/v1/lead-scoring/runs", headers=headers, json=embedded).status_code == 422
)
assert env["app_key"] not in caplog.text