"""Integration tests for key revocation (SPEC §4.5, §12). INSERT INTO ``gateway.revocations`` → AFTER-INSERT trigger fires ``pg_notify('key_revoked', key_id)`` → the gateway's revocation listener evicts the Redis cache entry for that key prefix → the next request with that key returns 401 within ~1 second. The key's DB status is also flipped to revoked so a full DB re-lookup likewise denies (defense in depth). """ from __future__ import annotations import asyncio import httpx import pytest from sqlalchemy import update from neuronetz_gateway.db.models import ApiKey, KeyStatus from neuronetz_gateway.db.repositories import RevocationRepository from tests.integration.conftest import IntegrationApp, IntegrationKey pytestmark = pytest.mark.asyncio async def _chat(client: httpx.AsyncClient, full_key: str) -> httpx.Response: return await client.post( "/api/chat", headers={"Authorization": f"Bearer {full_key}"}, json={ "model": "llama3.1:8b", "messages": [{"role": "user", "content": "hi"}], "stream": False, }, ) async def test_revoked_key_rejected_within_one_second( integration_app: IntegrationApp, client: httpx.AsyncClient, api_key: IntegrationKey, ) -> None: # Warm the auth cache by making a successful request first. ok = await _chat(client, api_key.full_key) assert ok.status_code == 200 # Revoke: flip status + insert into the outbox (fires NOTIFY via trigger). async with integration_app.sessionmaker() as session: await session.execute( update(ApiKey).where(ApiKey.id == api_key.key_id).values(status=KeyStatus.revoked) ) await RevocationRepository(session).insert(api_key.key_id, "test") await session.commit() # Poll up to 1s for the listener to evict the cache; the next request must # fail with 401 (or 403; "key revoked" is a sanitized auth failure). deadline = 1.0 waited = 0.0 step = 0.05 while waited < deadline: resp = await _chat(client, api_key.full_key) if resp.status_code != 200: assert resp.status_code in {401, 403} assert resp.json()["error"]["request_id"] return await asyncio.sleep(step) waited += step pytest.fail(f"revoked key still accepted after {deadline}s")