File size: 19,932 Bytes
cc036ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
"""Browser API contract tests using Schemathesis for OpenAPI compliance.

Validates that browser endpoints (session, navigation, interaction) conform to
their OpenAPI specification. Browser endpoints handle Playwright-based browser
automation for web scraping, form filling, and screenshots.

Contract test coverage:
- POST /api/browser/session - Create browser session
- GET /api/browser/sessions - List browser sessions
- DELETE /api/browser/session/{id} - Close browser session
- POST /api/browser/navigate - Navigate to URL
- POST /api/browser/click - Click element
- POST /api/browser/fill - Fill form field
- POST /api/browser/screenshot - Take screenshot
"""
import pytest
from fastapi.testclient import TestClient
from main_api_app import app
from tests.contract.conftest import schema
from unittest.mock import MagicMock, patch


class TestBrowserSessionContract:
    """Contract tests for browser session management endpoints."""

    def test_create_session_contracts(self):
        """Test POST /api/browser/session validates response schema."""
        if "/api/browser/session" in schema:
            operation = schema["/api/browser/session"]["POST"]
            with TestClient(app) as client:
                response = client.post("/api/browser/session")
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                # May return 200, 400, 401, 403, or 500 if Playwright not available
                assert response.status_code in [200, 400, 401, 403, 500, 503]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_create_session_with_config(self):
        """Test that session creation config conforms to schema."""
        if "/api/browser/session" in schema:
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/session",
                    json={
                        "headless": True,
                        "browser_type": "chromium"
                    }
                )
                assert response.status_code in [200, 400, 401, 403, 500, 503]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_list_sessions_contracts(self):
        """Test GET /api/browser/sessions validates response schema."""
        if "/api/browser/sessions" in schema:
            operation = schema["/api/browser/sessions"]["GET"]
            with TestClient(app) as client:
                response = client.get("/api/browser/sessions")
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 401, 403]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_close_session_contracts(self):
        """Test DELETE /api/browser/session/{id} validates response schema."""
        if "/api/browser/session/{session_id}" in schema:
            operation = schema["/api/browser/session/{session_id}"]["DELETE"]
            with TestClient(app) as client:
                response = client.delete("/api/browser/session/test-session-id")
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 204, 401, 403, 404]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_session_not_found(self):
        """Test that deleting non-existent session returns 404."""
        if "/api/browser/session/{session_id}" in schema:
            with TestClient(app) as client:
                response = client.delete("/api/browser/session/nonexistent-session-999")
                assert response.status_code in [200, 204, 401, 403, 404]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")


class TestBrowserNavigationContract:
    """Contract tests for browser navigation endpoints."""

    def test_navigate_contracts(self):
        """Test POST /api/browser/navigate validates response schema."""
        if "/api/browser/navigate" in schema:
            operation = schema["/api/browser/navigate"]["POST"]
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/navigate",
                    json={
                        "session_id": "test-session",
                        "url": "https://example.com"
                    }
                )
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_navigate_request_schema(self):
        """Test that URL parameter validation conforms to schema."""
        if "/api/browser/navigate" in schema:
            with TestClient(app) as client:
                # Test with valid URL
                response = client.post(
                    "/api/browser/navigate",
                    json={
                        "session_id": "test-session",
                        "url": "https://example.com/page"
                    }
                )
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_navigate_errors(self):
        """Test that 400/422 schemas work for invalid URLs."""
        if "/api/browser/navigate" in schema:
            with TestClient(app) as client:
                # Test with invalid URL (missing required field)
                response = client.post(
                    "/api/browser/navigate",
                    json={
                        "session_id": "test-session"
                        # Missing url field
                    }
                )
                # Should return 400 or 422
                assert response.status_code in [200, 400, 401, 403, 422, 500]

                # Test with malformed URL
                response = client.post(
                    "/api/browser/navigate",
                    json={
                        "session_id": "test-session",
                        "url": "not-a-valid-url"
                    }
                )
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_navigate_with_options(self):
        """Test that navigation options conform to schema."""
        if "/api/browser/navigate" in schema:
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/navigate",
                    json={
                        "session_id": "test-session",
                        "url": "https://example.com",
                        "wait_until": "networkidle",
                        "timeout": 30000
                    }
                )
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")


class TestBrowserInteractionContract:
    """Contract tests for browser interaction endpoints."""

    def test_click_contracts(self):
        """Test POST /api/browser/click validates response schema."""
        if "/api/browser/click" in schema:
            operation = schema["/api/browser/click"]["POST"]
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/click",
                    json={
                        "session_id": "test-session",
                        "selector": "#submit-button"
                    }
                )
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_fill_contracts(self):
        """Test POST /api/browser/fill validates response schema."""
        if "/api/browser/fill" in schema:
            operation = schema["/api/browser/fill"]["POST"]
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/fill",
                    json={
                        "session_id": "test-session",
                        "selector": "#username",
                        "value": "testuser"
                    }
                )
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_fill_multiple_fields(self):
        """Test that filling multiple fields conforms to schema."""
        if "/api/browser/fill" in schema:
            with TestClient(app) as client:
                # Test with fill array (if supported)
                response = client.post(
                    "/api/browser/fill",
                    json={
                        "session_id": "test-session",
                        "fields": [
                            {"selector": "#username", "value": "user"},
                            {"selector": "#password", "value": "pass"}
                        ]
                    }
                )
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_screenshot_contracts(self):
        """Test POST /api/browser/screenshot validates response schema."""
        if "/api/browser/screenshot" in schema:
            operation = schema["/api/browser/screenshot"]["POST"]
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/screenshot",
                    json={
                        "session_id": "test-session"
                    }
                )
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 400, 401, 403, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_screenshot_response(self):
        """Test that screenshot response includes Base64 image."""
        if "/api/browser/screenshot" in schema:
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/screenshot",
                    json={
                        "session_id": "test-session",
                        "format": "png"
                    }
                )
                # If successful, should have image data
                if response.status_code == 200:
                    json_resp = response.json()
                    # Should have base64 image data
                    assert "image" in json_resp or "data" in json_resp
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_screenshot_options(self):
        """Test that screenshot options conform to schema."""
        if "/api/browser/screenshot" in schema:
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/screenshot",
                    json={
                        "session_id": "test-session",
                        "format": "jpeg",
                        "quality": 80,
                        "full_page": True
                    }
                )
                assert response.status_code in [200, 400, 401, 403, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_execute_script_contracts(self):
        """Test POST /api/browser/execute validates response schema."""
        if "/api/browser/execute" in schema:
            operation = schema["/api/browser/execute"]["POST"]
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/execute",
                    json={
                        "session_id": "test-session",
                        "script": "document.title"
                    }
                )
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")


class TestBrowserGovernanceContract:
    """Contract tests for browser governance and permissions."""

    def test_governance_headers(self):
        """Test that X-Agent-Maturity header is processed."""
        with TestClient(app) as client:
            # Test with governance header
            if "/api/browser/session" in schema:
                response = client.post(
                    "/api/browser/session",
                    headers={"X-Agent-Maturity": "SUPERVISED"}
                )
                # Header may or may not be enforced in contract tests
                assert response.status_code in [200, 400, 401, 403, 500, 503]

    def test_permission_denied(self):
        """Test that 403 response conforms to schema."""
        # Browser operations require INTERN+ maturity
        # This test validates the 403 response schema
        if "/api/browser/session" in schema:
            with TestClient(app) as client:
                response = client.post("/api/browser/session")
                # If permission denied, should return 403
                if response.status_code == 403:
                    # Validate error response structure
                    json_resp = response.json()
                    assert "detail" in json_resp or "error" in json_resp
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_unauthorized(self):
        """Test that 401 response conforms to schema."""
        if "/api/browser/session" in schema:
            with TestClient(app) as client:
                response = client.post("/api/browser/session")
                # If unauthorized, should return 401
                if response.status_code == 401:
                    # Validate error response structure
                    json_resp = response.json()
                    assert "detail" in json_resp or "error" in json_resp
        else:
            pytest.skip("Endpoint not in OpenAPI schema")


class TestBrowserErrorHandlingContract:
    """Contract tests for browser error handling."""

    def test_session_timeout(self):
        """Test that session timeout returns appropriate error."""
        if "/api/browser/navigate" in schema:
            with TestClient(app) as client:
                # Test with expired session
                response = client.post(
                    "/api/browser/navigate",
                    json={
                        "session_id": "expired-session",
                        "url": "https://example.com"
                    }
                )
                # Should return error for expired session
                assert response.status_code in [200, 400, 401, 403, 404, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_element_not_found(self):
        """Test that missing element returns appropriate error."""
        if "/api/browser/click" in schema:
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/click",
                    json={
                        "session_id": "test-session",
                        "selector": "#nonexistent-element"
                    }
                )
                # Should return error if element not found
                assert response.status_code in [200, 400, 401, 403, 404, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_navigation_timeout(self):
        """Test that navigation timeout returns appropriate error."""
        if "/api/browser/navigate" in schema:
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/navigate",
                    json={
                        "session_id": "test-session",
                        "url": "https://example.com",
                        "timeout": 1  # 1ms timeout (will fail)
                    }
                )
                # Should return error for timeout
                assert response.status_code in [200, 400, 401, 403, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")


class TestBrowserCDPContract:
    """Contract tests for Chrome DevTools Protocol (CDP) endpoints."""

    def test_cdp_session_contracts(self):
        """Test CDP session creation validates schema."""
        if "/api/browser/cdp" in schema:
            operation = schema["/api/browser/cdp"]["POST"]
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/cdp",
                    json={
                        "session_id": "test-session"
                    }
                )
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 400, 401, 403, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")

    def test_cdp_execute_contracts(self):
        """Test CDP command execution validates schema."""
        # CDP allows sending arbitrary Chrome DevTools Protocol commands
        # Schema validation is more complex due to dynamic command structure
        if "/api/browser/cdp/execute" in schema:
            operation = schema["/api/browser/cdp/execute"]["POST"]
            with TestClient(app) as client:
                response = client.post(
                    "/api/browser/cdp/execute",
                    json={
                        "session_id": "test-session",
                        "command": "Page.navigate",
                        "params": {"url": "https://example.com"}
                    }
                )
                # Validate response against OpenAPI schema
                operation.validate_response(response)
                assert response.status_code in [200, 400, 401, 403, 422, 500]
        else:
            pytest.skip("Endpoint not in OpenAPI schema")


class TestBrowserInputStrategies:
    """Hypothesis strategies for browser-specific input generation."""

    def test_valid_url_strategy(self):
        """Test that URL strategy generates valid URLs."""
        # This documents the URL validation strategy for Hypothesis
        # Valid URLs should be absolute and well-formed
        valid_urls = [
            "https://example.com",
            "https://example.com/page",
            "https://example.com/page?query=value",
            "https://example.com:8080/page"
        ]
        # These should all pass URL validation
        for url in valid_urls:
            # In actual implementation, would use hypothesis.strategies
            assert url.startswith(("http://", "https://"))

    def test_selector_strategy(self):
        """Test that selector strategy generates valid CSS selectors."""
        # This documents the CSS selector validation strategy
        valid_selectors = [
            "#id",
            ".class",
            "tag",
            "#id .class",
            "tag[attr=value]",
            "parent > child"
        ]
        # These should all be valid CSS selectors
        for selector in valid_selectors:
            # In actual implementation, would use hypothesis.strategies
            assert len(selector) > 0