infrasynth-backend-kit/tests/test_audit/test_views.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

136 lines
5 KiB
Python

import pytest
from rest_framework import status
from infrasynth.audit.models import APIInteractionLog, ModelChangeLog, SecurityEvent
@pytest.fixture
def change_log(db, user, tenant):
return ModelChangeLog.objects.create(
tenant=tenant,
model_label="infrasynth_security.Role",
object_id="1",
action="create",
changes={"name": [None, "Test"]},
actor=user,
request_id="req-1",
)
@pytest.fixture
def api_log(db, user, tenant):
return APIInteractionLog.objects.create(
tenant=tenant,
method="GET",
path="/api/v1/features/",
status_code=200,
actor=user,
duration_ms=12,
request_id="req-2",
)
@pytest.fixture
def security_event(db, user, tenant):
return SecurityEvent.objects.create(
tenant=tenant,
event_type="login_failed",
actor=user,
ip_address="127.0.0.1",
metadata={"reason": "bad_password"},
request_id="req-3",
)
class TestModelChangeLogEndpoints:
def test_list_changes(self, authenticated_client, change_log):
resp = authenticated_client.get("/api/v1/audit/changes/", {"request_id": "req-1"})
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["count"] == 1
assert resp.json()["results"][0]["action"] == "create"
def test_retrieve_change(self, authenticated_client, change_log):
resp = authenticated_client.get(f"/api/v1/audit/changes/{change_log.pk}/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["changes"] == {"name": [None, "Test"]}
def test_requires_auth(self, api_client, change_log):
resp = api_client.get("/api/v1/audit/changes/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
def test_list_does_not_allow_create(self, authenticated_client):
resp = authenticated_client.post("/api/v1/audit/changes/", {"model_label": "x"}, format="json")
assert resp.status_code in (
status.HTTP_405_METHOD_NOT_ALLOWED,
status.HTTP_403_FORBIDDEN,
)
class TestAPIInteractionLogEndpoints:
def test_list_api_logs(self, authenticated_client, api_log):
resp = authenticated_client.get("/api/v1/audit/api-logs/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["count"] == 1
assert resp.json()["results"][0]["method"] == "GET"
def test_retrieve_api_log(self, authenticated_client, api_log):
resp = authenticated_client.get(f"/api/v1/audit/api-logs/{api_log.pk}/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["path"] == "/api/v1/features/"
def test_requires_auth(self, api_client, api_log):
resp = api_client.get("/api/v1/audit/api-logs/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
class TestSecurityEventEndpoints:
def test_list_security_events(self, authenticated_client, security_event):
resp = authenticated_client.get("/api/v1/audit/security-events/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["count"] == 1
assert resp.json()["results"][0]["event_type"] == "login_failed"
def test_retrieve_security_event(self, authenticated_client, security_event):
resp = authenticated_client.get(f"/api/v1/audit/security-events/{security_event.pk}/")
assert resp.status_code == status.HTTP_200_OK
assert resp.json()["metadata"] == {"reason": "bad_password"}
def test_requires_auth(self, api_client, security_event):
resp = api_client.get("/api/v1/audit/security-events/")
assert resp.status_code == status.HTTP_401_UNAUTHORIZED
class TestFiltering:
def test_filter_by_model_label(self, authenticated_client, db, tenant):
ModelChangeLog.objects.create(
tenant=tenant,
model_label="infrasynth_security.Role",
object_id="10",
action="create",
changes={},
request_id="x1",
)
ModelChangeLog.objects.create(
tenant=tenant,
model_label="infrasynth_security.Grant",
object_id="99",
action="create",
changes={},
request_id="x2",
)
resp = authenticated_client.get("/api/v1/audit/changes/", {"model_label": "infrasynth_security.Role"})
assert resp.json()["count"] == 1
def test_filter_by_action(self, authenticated_client, change_log, tenant):
ModelChangeLog.objects.create(
tenant=tenant,
model_label="infrasynth_security.Role",
object_id="77",
action="delete",
changes={},
request_id="req-4",
)
resp = authenticated_client.get("/api/v1/audit/changes/", {"action": "create"})
assert all(r["action"] == "create" for r in resp.json()["results"])
resp = authenticated_client.get("/api/v1/audit/changes/", {"action": "delete"})
assert all(r["action"] == "delete" for r in resp.json()["results"])