Files
gh-christianlouis-dmarq/backend/app/tests/test_public_api.py
T
2026-05-23 17:39:34 +02:00

106 lines
3.6 KiB
Python

from fastapi.testclient import TestClient
from app.models.api_token import APIToken
from app.services.api_tokens import READ_POSTURE_SCOPE, READ_REPORTS_SCOPE, create_api_token
from app.services.report_store import ReportStore
DOMAIN = "example.com"
MINIMAL_REPORT = {
"domain": DOMAIN,
"report_id": "public-api-001",
"org_name": "Test Org",
"policy": {"p": "none", "sp": "", "pct": "100"},
"records": [
{
"source_ip": "1.2.3.4",
"count": 5,
"disposition": "none",
"dkim_result": "pass",
"spf_result": "pass",
"dkim": [{"domain": DOMAIN, "result": "pass", "selector": "google"}],
"spf": [{"domain": DOMAIN, "result": "pass"}],
}
],
"summary": {"total_count": 5, "passed_count": 5, "failed_count": 0, "pass_rate": 100.0},
}
def _seed_report_store():
ReportStore.get_instance().add_report(MINIMAL_REPORT)
def test_public_reports_api_requires_scoped_token(client: TestClient, db_session):
"""Stable public report endpoints require scoped tokens and audit usage."""
_seed_report_store()
created = create_api_token(db_session, name="report bot", scopes=[READ_REPORTS_SCOPE])
missing = client.get(f"/api/v1/public/domains/{DOMAIN}/reports")
assert missing.status_code == 401
invalid = client.get(
f"/api/v1/public/domains/{DOMAIN}/reports",
headers={"X-API-Key": "not-valid"},
)
assert invalid.status_code == 401
response = client.get(
f"/api/v1/public/domains/{DOMAIN}/reports",
headers={"X-API-Key": created.secret},
)
assert response.status_code == 200
assert response.json()["reports"][0]["id"] == "public-api-001"
token = db_session.query(APIToken).filter(APIToken.id == created.token.id).one()
assert token.usage_count == 1
assert token.last_used_at is not None
assert token.last_used_ip
def test_public_api_rejects_token_without_required_scope(client: TestClient, db_session):
"""Tokens are least-privilege: reports scope cannot read posture payloads."""
_seed_report_store()
created = create_api_token(db_session, name="report bot", scopes=[READ_REPORTS_SCOPE])
response = client.get(
f"/api/v1/public/domains/{DOMAIN}/posture",
headers={"X-API-Key": created.secret},
)
assert response.status_code == 403
assert response.json()["detail"] == f"API token requires scope: {READ_POSTURE_SCOPE}"
def test_admin_can_create_list_and_revoke_api_tokens(authed_client: TestClient, db_session):
"""Admin token management never returns stored hashes and revocation disables access."""
_seed_report_store()
created = authed_client.post(
"/api/v1/api-tokens",
json={"name": "automation", "scopes": [READ_REPORTS_SCOPE]},
)
assert created.status_code == 201
body = created.json()
assert body["token"].startswith("dmarq_")
assert body["metadata"]["scopes"] == [READ_REPORTS_SCOPE]
assert "key_hash" not in body["metadata"]
listed = authed_client.get("/api/v1/api-tokens")
assert listed.status_code == 200
assert listed.json()["tokens"][0]["name"] == "automation"
assert "key_hash" not in listed.text
allowed = authed_client.get(
f"/api/v1/public/domains/{DOMAIN}/reports",
headers={"X-API-Key": body["token"]},
)
assert allowed.status_code == 200
revoked = authed_client.delete(f"/api/v1/api-tokens/{body['metadata']['id']}")
assert revoked.status_code == 200
denied = authed_client.get(
f"/api/v1/public/domains/{DOMAIN}/reports",
headers={"X-API-Key": body["token"]},
)
assert denied.status_code == 401