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