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.
224 lines
8.9 KiB
Python
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
|