First implementation API for PIP (Proyecto: Pharmacy Integration Platform)
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
"""E2E tests for API Key authentication flow.
|
||||
|
||||
Tests dual auth support (JWT Bearer + X-API-Key header),
|
||||
API Key CRUD, scope enforcement, and role-based access control.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
BASE_URL = os.environ.get("PIP_BASE_URL", "http://localhost:8000")
|
||||
ADMIN_CREDS = {"username": os.environ.get("PIP_ADMIN_USER", "synctest"), "password": os.environ.get("PIP_ADMIN_PASS", "Test123!")}
|
||||
|
||||
|
||||
async def get_admin_token(client: httpx.AsyncClient) -> str:
|
||||
r = await client.post("/api/v1/auth/token", json=ADMIN_CREDS)
|
||||
assert r.status_code == 200, f"Login failed: {r.status_code} {r.text[:200]}"
|
||||
return r.json()["access_token"]
|
||||
|
||||
|
||||
def bearer_headers(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
async def test_api_key_crud():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
token = await get_admin_token(c)
|
||||
h = bearer_headers(token)
|
||||
|
||||
clients = await c.get("/api/v1/api-clients", headers=h)
|
||||
assert clients.status_code == 200
|
||||
client_id = next((ci["id"] for ci in clients.json().get("items", []) if ci["name"] == "Test Client"), None)
|
||||
|
||||
if not client_id:
|
||||
resp = await c.post("/api/v1/api-clients", headers=h, json={
|
||||
"name": "Test Client",
|
||||
"description": "E2E test client",
|
||||
"scopes": ["pharmacies:read", "medications:read", "inventory:read"],
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
client_id = resp.json()["id"]
|
||||
|
||||
key_resp = await c.post(f"/api/v1/api-clients/{client_id}/keys", headers=h, json={"name": "E2E CRUD Key"})
|
||||
assert key_resp.status_code == 201, f"Key create failed: {key_resp.status_code} {key_resp.text[:200]}"
|
||||
raw_key = key_resp.json()["key"]
|
||||
key_prefix = key_resp.json()["key_prefix"]
|
||||
assert len(raw_key) > 20
|
||||
assert len(key_prefix) == 8
|
||||
|
||||
keys = await c.get(f"/api/v1/api-clients/{client_id}/keys", headers=h)
|
||||
assert keys.status_code == 200
|
||||
assert len(keys.json()) >= 1
|
||||
|
||||
print(" PASS: test_api_key_crud")
|
||||
|
||||
|
||||
async def test_api_key_read_access():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
token = await get_admin_token(c)
|
||||
h = bearer_headers(token)
|
||||
|
||||
clients = await c.get("/api/v1/api-clients", headers=h)
|
||||
client_id = next((ci["id"] for ci in clients.json().get("items", []) if ci["name"] == "Test Client"), None)
|
||||
kr = await c.post(f"/api/v1/api-clients/{client_id}/keys", headers=h, json={"name": "Read Access Key"})
|
||||
raw_key = kr.json()["key"]
|
||||
api_h = {"X-API-Key": raw_key}
|
||||
|
||||
r = await c.get("/api/v1/pharmacies", headers=api_h)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["total"] >= 0
|
||||
|
||||
r2 = await c.get("/api/v1/medications", headers=api_h)
|
||||
assert r2.status_code == 200
|
||||
|
||||
r3 = await c.get("/api/v1/inventory", headers=api_h)
|
||||
assert r3.status_code == 200
|
||||
|
||||
print(" PASS: test_api_key_read_access")
|
||||
|
||||
|
||||
async def test_api_key_write_blocked():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
token = await get_admin_token(c)
|
||||
h = bearer_headers(token)
|
||||
|
||||
clients = await c.get("/api/v1/api-clients", headers=h)
|
||||
client_id = next((ci["id"] for ci in clients.json().get("items", []) if ci["name"] == "Test Client"), None)
|
||||
kr = await c.post(f"/api/v1/api-clients/{client_id}/keys", headers=h, json={"name": "Write Block Key"})
|
||||
raw_key = kr.json()["key"]
|
||||
api_h = {"X-API-Key": raw_key}
|
||||
|
||||
r = await c.post("/api/v1/pharmacies", headers=api_h, json={"code": "BLOCKED", "name": "Should Fail"})
|
||||
assert r.status_code == 403, f"Expected 403, got {r.status_code}"
|
||||
|
||||
print(" PASS: test_api_key_write_blocked")
|
||||
|
||||
|
||||
async def test_invalid_api_key():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
r = await c.get("/api/v1/pharmacies", headers={"X-API-Key": "invalidkey12345"})
|
||||
assert r.status_code == 401
|
||||
|
||||
print(" PASS: test_invalid_api_key")
|
||||
|
||||
|
||||
async def test_no_auth_rejected():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
r = await c.get("/api/v1/pharmacies")
|
||||
assert r.status_code == 401
|
||||
|
||||
print(" PASS: test_no_auth_rejected")
|
||||
|
||||
|
||||
async def test_dual_auth_bearer_priority():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
token = await get_admin_token(c)
|
||||
h = bearer_headers(token)
|
||||
|
||||
clients = await c.get("/api/v1/api-clients", headers=h)
|
||||
client_id = next((ci["id"] for ci in clients.json().get("items", []) if ci["name"] == "Test Client"), None)
|
||||
kr = await c.post(f"/api/v1/api-clients/{client_id}/keys", headers=h, json={"name": "Dual Auth Key"})
|
||||
raw_key = kr.json()["key"]
|
||||
|
||||
dual_h = {"Authorization": f"Bearer {token}", "X-API-Key": raw_key}
|
||||
r = await c.get("/api/v1/pharmacies", headers=dual_h)
|
||||
assert r.status_code == 200
|
||||
|
||||
print(" PASS: test_dual_auth_bearer_priority")
|
||||
|
||||
|
||||
async def run_all():
|
||||
print("=== API Key Auth E2E Tests ===")
|
||||
await test_api_key_crud()
|
||||
await test_api_key_read_access()
|
||||
await test_api_key_write_blocked()
|
||||
await test_invalid_api_key()
|
||||
await test_no_auth_rejected()
|
||||
await test_dual_auth_bearer_priority()
|
||||
print("=== All API Key tests passed ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_all())
|
||||
@@ -0,0 +1,105 @@
|
||||
"""E2E tests for Rate Limiting middleware.
|
||||
|
||||
Tests that rate limiting returns 429 with proper headers
|
||||
when request count exceeds the configured limit.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
BASE_URL = os.environ.get("PIP_BASE_URL", "http://localhost:8000")
|
||||
ADMIN_CREDS = {"username": os.environ.get("PIP_ADMIN_USER", "synctest"), "password": os.environ.get("PIP_ADMIN_PASS", "Test123!")}
|
||||
|
||||
|
||||
async def get_admin_token(client: httpx.AsyncClient) -> str:
|
||||
r = await client.post("/api/v1/auth/token", json=ADMIN_CREDS)
|
||||
assert r.status_code == 200
|
||||
return r.json()["access_token"]
|
||||
|
||||
|
||||
def bearer_headers(token: str) -> dict:
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
async def test_rate_limit_headers():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
token = await get_admin_token(c)
|
||||
h = bearer_headers(token)
|
||||
|
||||
r = await c.get("/api/v1/pharmacies", headers=h)
|
||||
assert r.status_code == 200
|
||||
assert "x-ratelimit-limit" in r.headers, f"Missing X-RateLimit-Limit header. Got: {list(r.headers.keys())}"
|
||||
assert "x-ratelimit-remaining" in r.headers
|
||||
assert "x-ratelimit-reset" in r.headers
|
||||
|
||||
limit = int(r.headers["x-ratelimit-limit"])
|
||||
assert limit > 0
|
||||
|
||||
print(f" PASS: test_rate_limit_headers (limit={limit}/min)")
|
||||
|
||||
|
||||
async def test_rate_limit_429():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
token = await get_admin_token(c)
|
||||
h = bearer_headers(token)
|
||||
|
||||
r0 = await c.get("/api/v1/pharmacies", headers=h)
|
||||
limit = int(r0.headers["x-ratelimit-limit"])
|
||||
|
||||
successes = 0
|
||||
blocked = 0
|
||||
for i in range(limit + 10):
|
||||
r = await c.get("/api/v1/pharmacies", headers=h)
|
||||
if r.status_code == 200:
|
||||
successes += 1
|
||||
elif r.status_code == 429:
|
||||
blocked += 1
|
||||
assert "retry-after" in r.headers, "429 response missing Retry-After header"
|
||||
break
|
||||
|
||||
assert blocked > 0, f"Rate limit was not triggered after {limit + 10} requests"
|
||||
|
||||
print(f" PASS: test_rate_limit_429 (blocked after {successes} requests)")
|
||||
|
||||
|
||||
async def test_rate_limit_exempt_paths():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL) as c:
|
||||
r = await c.get("/docs")
|
||||
assert r.status_code == 200
|
||||
assert "x-ratelimit-limit" not in r.headers, "Docs should be exempt from rate limiting"
|
||||
|
||||
print(" PASS: test_rate_limit_exempt_paths")
|
||||
|
||||
|
||||
async def test_rate_limit_per_client():
|
||||
async with httpx.AsyncClient(base_url=BASE_URL, timeout=30.0) as c:
|
||||
token = await get_admin_token(c)
|
||||
h = bearer_headers(token)
|
||||
|
||||
r0 = await c.get("/api/v1/pharmacies", headers=h)
|
||||
limit = int(r0.headers["x-ratelimit-limit"])
|
||||
|
||||
for _ in range(limit + 5):
|
||||
r = await c.get("/api/v1/pharmacies", headers=h)
|
||||
if r.status_code == 429:
|
||||
break
|
||||
|
||||
r2 = await c.get("/api/v1/system/health")
|
||||
assert r2.status_code == 200, "Health endpoint should work regardless of rate limit on other paths"
|
||||
|
||||
print(" PASS: test_rate_limit_per_client")
|
||||
|
||||
|
||||
async def run_all():
|
||||
print("=== Rate Limiting E2E Tests ===")
|
||||
await test_rate_limit_headers()
|
||||
await test_rate_limit_429()
|
||||
await test_rate_limit_exempt_paths()
|
||||
await test_rate_limit_per_client()
|
||||
print("=== All Rate Limiting tests passed ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_all())
|
||||
Reference in New Issue
Block a user