File size: 15,716 Bytes
aef804e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
"""
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]  # May be unauthorized
        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')

        # Error responses should have clear messages
        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."""
        # Test 400 Bad Request
        response = client.post(
            '/api/v1/agents/execute',
            json={'invalid': 'data'}
        )

        if response.status_code == 400:
            error_data = response.json()
            # Error should have a human-readable message
            assert 'detail' in error_data or 'message' in error_data
            # Message should not be empty or just error code
            message = error_data.get('detail', error_data.get('message', ''))
            assert len(message) > 10  # More than just error code

    def test_api_supports_head_requests_for_resources(self, client: TestClient):
        """Test that HEAD method is supported for resource endpoints."""
        # HEAD request should return same headers as GET
        response = client.head('/api/v1/agents')

        # HEAD should return headers without body
        assert response.status_code in [200, 401, 403]
        assert 'Content-Type' in response.headers
        assert len(response.content) == 0  # No body in HEAD response

    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')

        # Check for common rate limit headers
        rate_limit_headers = [
            'X-RateLimit-Limit',
            'X-RateLimit-Remaining',
            'X-RateLimit-Reset',
            'RateLimit-Limit',
            'RateLimit-Remaining',
            'RateLimit-Reset'
        ]

        # At least one rate limit header should be present if rate limiting is enabled
        has_rate_limit = any(header in response.headers for header in rate_limit_headers)

        # If rate limiting is enabled, headers should be readable
        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."""
        # Test a list endpoint that might have pagination
        response = client.get('/api/v1/agents')

        # Check for common pagination headers
        pagination_headers = [
            'X-Total-Count',
            'X-Page',
            'X-Per-Page',
            'X-Total-Pages',
            'Link'
        ]

        # If pagination is present, headers should be clear
        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')

        # Content-Language header should indicate response language
        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]:
            # Error should be JSON
            assert 'application/json' in response.headers['Content-Type']

            # Error should have structure
            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()

            # Error should explain what was wrong
            assert 'detail' in error_data or 'message' in error_data
            message = error_data.get('detail', error_data.get('message', ''))

            # Message should be descriptive
            assert len(message) > 5

    def test_api_success_responses_are_consistent(self, client: TestClient):
        """Test that success responses have consistent structure."""
        # Try to get agents list
        response = client.get('/api/v1/agents')

        if response.status_code == 200:
            data = response.json()

            # Should be a list or dict with data field
            assert isinstance(data, (dict, list))

            if isinstance(data, dict):
                # Common response fields
                possible_keys = ['data', 'results', 'agents', 'items']
                has_data_key = any(key in data for key in possible_keys)
                # Not all endpoints need these keys, so we don't assert

    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()

            # Check for timestamp in response headers or body
            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']
                ))
            )
            # Timestamps are good practice but not always required

    def test_api_cors_headers_are_accessible(self, client: TestClient):
        """Test that CORS headers are present and readable."""
        response = client.get('/api/v1/agents')

        # Check for common CORS headers
        cors_headers = [
            'Access-Control-Allow-Origin',
            'Access-Control-Allow-Methods',
            'Access-Control-Allow-Headers',
            'Access-Control-Max-Age'
        ]

        # If CORS is enabled, headers should be present
        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()

            # Error message should be in natural language
            message = error_data.get('detail', error_data.get('message', ''))

            # Should not be just error codes
            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."""
        # Request JSON response
        response = client.get(
            '/api/v1/agents',
            headers={'Accept': 'application/json'}
        )

        # Should return 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."""
        # Request with Accept-Encoding
        response = client.get(
            '/api/v1/agents',
            headers={'Accept-Encoding': 'gzip, deflate'}
        )

        # If response is large enough, should be compressed
        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 help with debugging accessibility issues
        request_id_headers = [
            'X-Request-ID',
            'X-Correlation-ID',
            'Request-ID'
        ]

        # At least one request ID header is good practice
        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]  # Up or degraded

        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 response should indicate status
            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': ''}  # Empty name should fail validation
        )

        if response.status_code == 422:
            error_data = response.json()

            # Validation errors should list specific issues
            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')

        # Version can be in URL, header, or response body
        has_version = (
            'X-API-Version' in response.headers or
            'API-Version' in response.headers
        )

        # API version in URL is sufficient (we use /api/v1/)
        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."""
        # This test would require triggering rate limit
        # For now, we test the structure

        # If rate limited, should return 429 with clear message
        # (We can't easily trigger this in tests)

        # Placeholder for rate limit testing
        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."""
        # If there are image/chart endpoints, they should include alt text
        # This is a placeholder for chart/canvas endpoints

        # Test canvas endpoint if it exists
        response = client.get('/api/v1/canvas/test-canvas-id')

        # If canvas exists, should include alt text or description
        if response.status_code == 200:
            data = response.json()

            # Check for alt text or description fields
            has_alt_text = (
                'alt_text' in data or
                'description' in data or
                'title' in data
            )

            # Not all canvas types need alt text, but it's good practice

    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()

            # Error should be in plain text, not HTML
            assert 'application/json' in response.headers.get('Content-Type', '')

            # Error message should be self-explanatory
            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')

        # Should use standard HTTP headers
        assert 'Content-Type' in response.headers

        # Should use appropriate status codes
        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()

        # Response should be reasonably fast (< 5 seconds)
        # This is important for users with assistive technology
        assert (end - start) < 5.0

    def test_api_timeout_handling(self, client: TestClient):
        """Test that API handles timeouts gracefully."""
        # This would require mocking slow responses
        # For now, we test the structure

        # Placeholder for timeout testing
        assert True

# Note: client fixture is provided by backend/tests/integration/conftest.py