"""Integration tests for channels audit_router endpoints.""" from __future__ import annotations import httpx import pytest from .conftest import BASE_URL, NON_EXISTENT_LOG_ID, NON_EXISTENT_TASK_ID pytestmark = [pytest.mark.asyncio, pytest.mark.integration] AUDIT_URL = f"{BASE_URL}/audit" # ============================================================================= # === Auth three-tier for GET /audit/logs === # ============================================================================= async def test_query_audit_logs_requires_auth(test_client: httpx.AsyncClient): # Act response = await test_client.get(f"{AUDIT_URL}/logs") # Assert assert response.status_code == 401, response.text async def test_query_audit_logs_requires_admin( test_client: httpx.AsyncClient, standard_user ): # Act response = await test_client.get( f"{AUDIT_URL}/logs", headers=standard_user["headers"] ) # Assert assert response.status_code == 403, response.text async def test_admin_can_query_audit_logs( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get(f"{AUDIT_URL}/logs", headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert "data" in payload # ============================================================================= # === Pagination validation for GET /audit/logs === # ============================================================================= async def test_query_audit_logs_rejects_limit_zero( test_client: httpx.AsyncClient, admin_headers ): # Act — limit=0 violates ge=1 response = await test_client.get( f"{AUDIT_URL}/logs", params={"limit": 0}, headers=admin_headers ) # Assert assert response.status_code == 422, response.text async def test_query_audit_logs_rejects_limit_over_max( test_client: httpx.AsyncClient, admin_headers ): # Act — limit=501 violates le=500 response = await test_client.get( f"{AUDIT_URL}/logs", params={"limit": 501}, headers=admin_headers ) # Assert assert response.status_code == 422, response.text async def test_query_audit_logs_rejects_negative_offset( test_client: httpx.AsyncClient, admin_headers ): # Act — offset=-1 violates ge=0 response = await test_client.get( f"{AUDIT_URL}/logs", params={"offset": -1}, headers=admin_headers ) # Assert assert response.status_code == 422, response.text # ============================================================================= # === GET /audit/logs/stats and /audit/operations === # ============================================================================= async def test_admin_can_get_audit_log_stats( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get(f"{AUDIT_URL}/logs/stats", headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert "data" in payload async def test_admin_can_list_audit_operations( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get(f"{AUDIT_URL}/operations", headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert "data" in payload # ============================================================================= # === POST /audit/export (create export task) === # ============================================================================= async def test_admin_can_create_audit_export_with_empty_body( test_client: httpx.AsyncClient, admin_headers ): # Arrange — all fields optional, empty dict is valid body: dict = {} # Act response = await test_client.post( f"{AUDIT_URL}/export", json=body, headers=admin_headers ) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert "data" in payload # ============================================================================= # === GET /audit/export (list export tasks) === # ============================================================================= async def test_admin_can_list_export_tasks( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get(f"{AUDIT_URL}/export", headers=admin_headers) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert "data" in payload # ============================================================================= # === GET /audit/retention-policy === # ============================================================================= async def test_admin_can_get_retention_policy( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get( f"{AUDIT_URL}/retention-policy", headers=admin_headers ) # Assert assert response.status_code == 200, response.text payload = response.json() assert payload["success"] is True assert "data" in payload # ============================================================================= # === PUT /audit/retention-policy (superadmin only) === # ============================================================================= async def test_update_retention_policy_requires_auth(test_client: httpx.AsyncClient): # Arrange body = {"default_retention_days": 90} # Act response = await test_client.put(f"{AUDIT_URL}/retention-policy", json=body) # Assert assert response.status_code == 401, response.text async def test_update_retention_policy_rejects_standard_user( test_client: httpx.AsyncClient, standard_user ): # Arrange body = {"default_retention_days": 90} # Act response = await test_client.put( f"{AUDIT_URL}/retention-policy", json=body, headers=standard_user["headers"], ) # Assert assert response.status_code == 403, response.text async def test_update_retention_policy_requires_superadmin( test_client: httpx.AsyncClient, admin_headers ): # Arrange — admin role (not superadmin) should be rejected body = {"default_retention_days": 90} # Act response = await test_client.put( f"{AUDIT_URL}/retention-policy", json=body, headers=admin_headers ) # Assert assert response.status_code == 403, response.text async def test_update_retention_policy_rejects_archive_before_ge_retention( test_client: httpx.AsyncClient, admin_headers ): # Arrange — auto_archive_before_days >= default_retention_days violates # dispatch-stage rule (RULE_VIOLATION -> 422) body = { "default_retention_days": 10, "auto_archive_before_days": 10, } # Act response = await test_client.put( f"{AUDIT_URL}/retention-policy", json=body, headers=admin_headers ) # Assert assert response.status_code == 422, response.text # ============================================================================= # === 404 paths for non-existent resources === # ============================================================================= async def test_get_audit_log_returns_404_for_nonexistent( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get( f"{AUDIT_URL}/logs/{NON_EXISTENT_LOG_ID}", headers=admin_headers ) # Assert assert response.status_code == 404, response.text async def test_get_audit_export_task_returns_404_for_nonexistent( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get( f"{AUDIT_URL}/export/{NON_EXISTENT_TASK_ID}", headers=admin_headers ) # Assert assert response.status_code == 404, response.text async def test_cancel_audit_export_task_returns_404_for_nonexistent( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.delete( f"{AUDIT_URL}/export/{NON_EXISTENT_TASK_ID}", headers=admin_headers ) # Assert assert response.status_code == 404, response.text async def test_download_audit_export_returns_404_for_nonexistent( test_client: httpx.AsyncClient, admin_headers ): # Act response = await test_client.get( f"{AUDIT_URL}/export/{NON_EXISTENT_TASK_ID}/download", headers=admin_headers, ) # Assert assert response.status_code == 404, response.text