infrasynth-backend-kit/tests/test_api/test_conventions.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

94 lines
3.5 KiB
Python

"""Cursor pagination + request id + exception handler (API-STANDARD.md §4-§7)."""
import pytest
from django.http import HttpResponse
from django.test import RequestFactory
from rest_framework.exceptions import ValidationError
from infrasynth.api.exceptions import envelope_exception_handler
from infrasynth.api.middleware import RequestIdMiddleware
from infrasynth.api.pagination import CursorPagination
from infrasynth.shared.exceptions import EntitlementError
class TestCursorPagination:
def test_configuration(self):
pagination = CursorPagination()
assert pagination.page_size == 25
assert pagination.max_page_size == 100
assert pagination.page_size_query_param == "pageSize"
assert pagination.ordering == "-pk"
def test_response_shape(self):
# get_paginated_response depends on cursor state set during pagination;
# the envelope renderer test covers the wire shape end-to-end.
pagination = CursorPagination()
assert callable(pagination.get_paginated_response)
class TestRequestIdMiddleware:
def test_generates_request_id(self):
request = RequestFactory().get("/")
response = RequestIdMiddleware(lambda r: HttpResponse("ok"))(request)
assert response["X-Request-Id"] == request.request_id
assert request.request_id.startswith("req_")
def test_echoes_supplied_request_id(self):
request = RequestFactory().get("/", HTTP_X_REQUEST_ID="req_abc")
response = RequestIdMiddleware(lambda r: HttpResponse("ok"))(request)
assert response["X-Request-Id"] == "req_abc"
assert request.request_id == "req_abc"
class TestEnvelopeExceptionHandler:
def test_app_error_maps_to_code_and_status(self):
response = envelope_exception_handler(
EntitlementError(code="ENTITLEMENT_PLAN_UPGRADE_REQUIRED", app="helpdesk", feature="tickets"),
{},
)
assert response.status_code == 402
assert response.data["code"] == "ENTITLEMENT_PLAN_UPGRADE_REQUIRED"
assert response.data["details"][0]["app"] == "helpdesk"
assert response.data["details"][0]["feature"] == "tickets"
def test_drf_validation_error_flattens_details(self):
response = envelope_exception_handler(ValidationError({"name": ["required"]}), {})
assert response.status_code == 400
assert response.data["code"] == "VALIDATION_ERROR"
assert response.data["details"] == [{"field": "name", "issue": "required"}]
def test_unexpected_exception_is_not_masked(self):
assert envelope_exception_handler(RuntimeError("boom"), {}) is None
@pytest.mark.django_db
class TestIdempotency:
def test_replays_first_successful_response(self):
from django.core.cache import cache
from rest_framework.response import Response
from infrasynth.api.idempotency import idempotent
cache.clear()
class _View:
def __init__(self):
self.calls = 0
@idempotent
def post(self, request):
self.calls += 1
return Response({"n": self.calls})
class _Request:
method = "POST"
path = "/api/v1/billing/checkout/"
headers = {"Idempotency-Key": "key-1"}
view = _View()
first = view.post(_Request())
second = view.post(_Request())
assert first.data == {"n": 1}
assert second.data == {"n": 1}
assert second["Idempotent-Replay"] == "true"
assert view.calls == 1