| """ |
| API accessibility header tests |
| |
| Tests verify that API responses include proper accessibility headers: |
| - Content-Type headers for all responses |
| - Accessible error responses with clear messages |
| - HEAD method support for resources |
| - Rate limit headers that are readable |
| - Clear pagination headers |
| - Content-Language headers |
| - Alt text for image endpoints |
| """ |
|
|
| import pytest |
| from fastapi.testclient import TestClient |
| from typing import Dict, Any |
|
|
|
|
| class TestAPIResponseHeaders: |
| """Test suite for API accessibility headers""" |
|
|
| def test_api_returns_content_type_header(self, client: TestClient): |
| """Test that API responses include Content-Type header.""" |
| response = client.get('/api/v1/agents') |
|
|
| assert response.status_code in [200, 401, 403] |
| assert 'Content-Type' in response.headers |
| assert 'application/json' in response.headers['Content-Type'] |
|
|
| def test_api_returns_accessible_error_responses(self, client: TestClient): |
| """Test that error responses have clear, accessible messages.""" |
| response = client.get('/api/v1/agents/nonexistent') |
|
|
| |
| if response.status_code == 404: |
| assert 'application/json' in response.headers['Content-Type'] |
| error_data = response.json() |
| assert 'detail' in error_data or 'message' in error_data |
|
|
| def test_api_error_messages_are_human_readable(self, client: TestClient): |
| """Test that error messages are clear and actionable.""" |
| |
| response = client.post( |
| '/api/v1/agents/execute', |
| json={'invalid': 'data'} |
| ) |
|
|
| if response.status_code == 400: |
| error_data = response.json() |
| |
| assert 'detail' in error_data or 'message' in error_data |
| |
| message = error_data.get('detail', error_data.get('message', '')) |
| assert len(message) > 10 |
|
|
| def test_api_supports_head_requests_for_resources(self, client: TestClient): |
| """Test that HEAD method is supported for resource endpoints.""" |
| |
| response = client.head('/api/v1/agents') |
|
|
| |
| assert response.status_code in [200, 401, 403] |
| assert 'Content-Type' in response.headers |
| assert len(response.content) == 0 |
|
|
| def test_api_rate_limit_headers_are_accessible(self, client: TestClient): |
| """Test that rate limit information is in readable headers.""" |
| response = client.get('/api/v1/agents') |
|
|
| |
| rate_limit_headers = [ |
| 'X-RateLimit-Limit', |
| 'X-RateLimit-Remaining', |
| 'X-RateLimit-Reset', |
| 'RateLimit-Limit', |
| 'RateLimit-Remaining', |
| 'RateLimit-Reset' |
| ] |
|
|
| |
| has_rate_limit = any(header in response.headers for header in rate_limit_headers) |
|
|
| |
| if has_rate_limit: |
| for header in rate_limit_headers: |
| if header in response.headers: |
| value = response.headers[header] |
| assert value is not None |
| assert len(value) > 0 |
|
|
| def test_api_pagination_headers_are_clear(self, client: TestClient): |
| """Test that pagination information is in clear headers.""" |
| |
| response = client.get('/api/v1/agents') |
|
|
| |
| pagination_headers = [ |
| 'X-Total-Count', |
| 'X-Page', |
| 'X-Per-Page', |
| 'X-Total-Pages', |
| 'Link' |
| ] |
|
|
| |
| for header in pagination_headers: |
| if header in response.headers: |
| value = response.headers[header] |
| assert value is not None |
| assert len(value) > 0 |
|
|
| def test_api_response_language_is_consistent(self, client: TestClient): |
| """Test that Content-Language header is present.""" |
| response = client.get('/api/v1/agents') |
|
|
| |
| if 'Content-Language' in response.headers: |
| lang = response.headers['Content-Language'] |
| assert lang in ['en', 'en-US', 'en-GB'] or lang.startswith('en') |
|
|
| def test_api_returns_structured_json_errors(self, client: TestClient): |
| """Test that errors are returned as structured JSON.""" |
| response = client.get('/api/v1/agents/nonexistent-id-12345') |
|
|
| if response.status_code in [400, 404, 422]: |
| |
| assert 'application/json' in response.headers['Content-Type'] |
|
|
| |
| error_data = response.json() |
| assert isinstance(error_data, dict) |
|
|
| def test_api_error_responses_include_helpful_info(self, client: TestClient): |
| """Test that error responses include helpful information.""" |
| response = client.get('/api/v1/agents/nonexistent-id-12345') |
|
|
| if response.status_code == 404: |
| error_data = response.json() |
|
|
| |
| assert 'detail' in error_data or 'message' in error_data |
| message = error_data.get('detail', error_data.get('message', '')) |
|
|
| |
| assert len(message) > 5 |
|
|
| def test_api_success_responses_are_consistent(self, client: TestClient): |
| """Test that success responses have consistent structure.""" |
| |
| response = client.get('/api/v1/agents') |
|
|
| if response.status_code == 200: |
| data = response.json() |
|
|
| |
| assert isinstance(data, (dict, list)) |
|
|
| if isinstance(data, dict): |
| |
| possible_keys = ['data', 'results', 'agents', 'items'] |
| has_data_key = any(key in data for key in possible_keys) |
| |
|
|
| def test_api_includes_timestamp_in_responses(self, client: TestClient): |
| """Test that API responses include timestamp information.""" |
| response = client.get('/api/v1/agents') |
|
|
| if response.status_code == 200: |
| data = response.json() |
|
|
| |
| has_timestamp = ( |
| 'Date' in response.headers or |
| 'Last-Modified' in response.headers or |
| (isinstance(data, dict) and any( |
| key in data for key in ['timestamp', 'created_at', 'updated_at', 'date'] |
| )) |
| ) |
| |
|
|
| def test_api_cors_headers_are_accessible(self, client: TestClient): |
| """Test that CORS headers are present and readable.""" |
| response = client.get('/api/v1/agents') |
|
|
| |
| cors_headers = [ |
| 'Access-Control-Allow-Origin', |
| 'Access-Control-Allow-Methods', |
| 'Access-Control-Allow-Headers', |
| 'Access-Control-Max-Age' |
| ] |
|
|
| |
| for header in cors_headers: |
| if header in response.headers: |
| value = response.headers[header] |
| assert value is not None |
| assert len(value) > 0 |
|
|
| def test_api_error_codes_are_explanatory(self, client: TestClient): |
| """Test that error codes are explanatory, not cryptic.""" |
| response = client.get('/api/v1/agents/nonexistent-id-12345') |
|
|
| if response.status_code == 404: |
| error_data = response.json() |
|
|
| |
| message = error_data.get('detail', error_data.get('message', '')) |
|
|
| |
| assert not message.startswith('E') |
| assert not message.startswith('ERR_') |
|
|
| def test_api_supports_content_negotiation(self, client: TestClient): |
| """Test that API supports content negotiation.""" |
| |
| response = client.get( |
| '/api/v1/agents', |
| headers={'Accept': 'application/json'} |
| ) |
|
|
| |
| assert 'Content-Type' in response.headers |
| assert 'application/json' in response.headers['Content-Type'] |
|
|
| def test_api_responses_are_gzipped_when_appropriate(self, client: TestClient): |
| """Test that API supports compression for large responses.""" |
| |
| response = client.get( |
| '/api/v1/agents', |
| headers={'Accept-Encoding': 'gzip, deflate'} |
| ) |
|
|
| |
| if 'Content-Encoding' in response.headers: |
| encoding = response.headers['Content-Encoding'] |
| assert encoding in ['gzip', 'deflate', 'br'] |
|
|
| def test_api_includes_request_id_in_headers(self, client: TestClient): |
| """Test that API includes request ID for debugging.""" |
| response = client.get('/api/v1/agents') |
|
|
| |
| request_id_headers = [ |
| 'X-Request-ID', |
| 'X-Correlation-ID', |
| 'Request-ID' |
| ] |
|
|
| |
| has_request_id = any(header in response.headers for header in request_id_headers) |
|
|
| if has_request_id: |
| for header in request_id_headers: |
| if header in response.headers: |
| value = response.headers[header] |
| assert value is not None |
| assert len(value) > 0 |
|
|
| def test_api_health_endpoint_accessible(self, client: TestClient): |
| """Test that health endpoint is accessible and clear.""" |
| response = client.get('/health/live') |
|
|
| assert response.status_code == 200 |
| assert 'application/json' in response.headers.get('Content-Type', '') |
|
|
| data = response.json() |
| assert isinstance(data, dict) |
|
|
| def test_api_readiness_endpoint_accessible(self, client: TestClient): |
| """Test that readiness endpoint includes service status.""" |
| response = client.get('/health/ready') |
|
|
| assert response.status_code in [200, 503] |
|
|
| if response.status_code == 200: |
| data = response.json() |
| assert isinstance(data, dict) |
|
|
| def test_api_error_responses_include_status_code(self, client: TestClient): |
| """Test that error responses include HTTP status context.""" |
| response = client.post( |
| '/api/v1/agents/execute', |
| json={'invalid': 'data'} |
| ) |
|
|
| if response.status_code == 400: |
| |
| error_data = response.json() |
| assert 'detail' in error_data or 'message' in error_data |
|
|
| def test_api_validation_errors_are_clear(self, client: TestClient): |
| """Test that validation errors provide specific feedback.""" |
| response = client.post( |
| '/api/v1/agents', |
| json={'name': ''} |
| ) |
|
|
| if response.status_code == 422: |
| error_data = response.json() |
|
|
| |
| assert 'detail' in error_data or 'errors' in error_data |
|
|
| def test_api_responses_include_api_version(self, client: TestClient): |
| """Test that API version is indicated in responses.""" |
| response = client.get('/api/v1/agents') |
|
|
| |
| has_version = ( |
| 'X-API-Version' in response.headers or |
| 'API-Version' in response.headers |
| ) |
|
|
| |
| assert '/v1/' in response.request.url or has_version |
|
|
| def test_api_rate_limit_exceeded_clear(self, client: TestClient): |
| """Test that rate limit exceeded is clearly indicated.""" |
| |
| |
|
|
| |
| |
|
|
| |
| assert True |
|
|
|
|
| class TestAPIAccessibilityForAssistiveTechnology: |
| """Test suite for API accessibility for assistive technology users""" |
|
|
| def test_api_alt_text_in_image_endpoints(self, client: TestClient): |
| """Test that image endpoints include alt text metadata.""" |
| |
| |
|
|
| |
| response = client.get('/api/v1/canvas/test-canvas-id') |
|
|
| |
| if response.status_code == 200: |
| data = response.json() |
|
|
| |
| has_alt_text = ( |
| 'alt_text' in data or |
| 'description' in data or |
| 'title' in data |
| ) |
|
|
| |
|
|
| def test_api_screen_reader_friendly_errors(self, client: TestClient): |
| """Test that errors are screen reader friendly.""" |
| response = client.get('/api/v1/agents/nonexistent') |
|
|
| if response.status_code == 404: |
| error_data = response.json() |
|
|
| |
| assert 'application/json' in response.headers.get('Content-Type', '') |
|
|
| |
| message = error_data.get('detail', error_data.get('message', '')) |
| assert len(message) > 0 |
|
|
| def test_api_semantic_headers(self, client: TestClient): |
| """Test that API uses semantic HTTP headers.""" |
| response = client.get('/api/v1/agents') |
|
|
| |
| assert 'Content-Type' in response.headers |
|
|
| |
| assert response.status_code in [200, 201, 400, 401, 403, 404, 422, 500] |
|
|
|
|
| class TestAPIResponseTimeAccessibility: |
| """Test suite for API response time considerations""" |
|
|
| def test_api_responses_are_reasonably_fast(self, client: TestClient): |
| """Test that API responses are fast enough for accessibility.""" |
| import time |
|
|
| start = time.time() |
| response = client.get('/api/v1/agents') |
| end = time.time() |
|
|
| |
| |
| assert (end - start) < 5.0 |
|
|
| def test_api_timeout_handling(self, client: TestClient): |
| """Test that API handles timeouts gracefully.""" |
| |
| |
|
|
| |
| assert True |
|
|
| |
|
|