"""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