| """Canvas API contract tests using Schemathesis for OpenAPI compliance. |
| |
| Validates that canvas endpoints (submit, query, list) conform to their |
| OpenAPI specification. Canvas endpoints handle visual presentations including |
| charts, forms, markdown, sheets, and other canvas types. |
| |
| Contract test coverage: |
| - POST /api/canvas/submit - Submit canvas with form data |
| - GET /api/canvas/{id} - Get canvas by ID |
| - GET /api/canvas/ - List canvases |
| - Various canvas type schemas (chart, form, markdown, sheet) |
| """ |
| import pytest |
| from fastapi.testclient import TestClient |
| from main_api_app import app |
| from tests.contract.conftest import schema |
|
|
|
|
| class TestCanvasSubmissionContract: |
| """Contract tests for POST /api/canvas/submit endpoint.""" |
|
|
| def test_submit_canvas_contracts(self): |
| """Test POST /api/canvas/submit validates response schema.""" |
| |
| if "/api/canvas/submit" in schema: |
| operation = schema["/api/canvas/submit"]["POST"] |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/canvas/submit", |
| json={ |
| "canvas_id": "test-canvas", |
| "form_data": {} |
| } |
| ) |
| |
| operation.validate_response(response) |
| |
| assert response.status_code in [200, 400, 401, 403, 404, 422] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_submit_request_schema(self): |
| """Test that form submission schema is enforced.""" |
| if "/api/canvas/submit" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/canvas/submit", |
| json={ |
| "canvas_id": "schema-test-canvas", |
| "form_data": { |
| "field1": "value1", |
| "field2": 123 |
| } |
| } |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 404, 422] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_submit_success_response(self): |
| """Test that 200 response includes audit_id.""" |
| if "/api/canvas/submit" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/canvas/submit", |
| json={ |
| "canvas_id": "success-test-canvas", |
| "form_data": {} |
| } |
| ) |
| |
| if response.status_code == 200: |
| |
| json_resp = response.json() |
| assert "audit_id" in json_resp or "canvas_id" in json_resp |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_submit_validation_errors(self): |
| """Test that 400/422 responses conform to schema.""" |
| if "/api/canvas/submit" in schema: |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/canvas/submit", |
| json={ |
| |
| "form_data": {} |
| } |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 422] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_submit_invalid_canvas_id(self): |
| """Test that invalid canvas_id format returns 422.""" |
| if "/api/canvas/submit" in schema: |
| with TestClient(app) as client: |
| response = client.post( |
| "/api/canvas/submit", |
| json={ |
| "canvas_id": 123, |
| "form_data": {} |
| } |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 422] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestCanvasQueryContract: |
| """Contract tests for GET /api/canvas/{id} and GET /api/canvas/ endpoints.""" |
|
|
| def test_get_canvas_contracts(self): |
| """Test GET /api/canvas/{id} validates response schema.""" |
| if "/api/canvas/{canvas_id}" in schema: |
| operation = schema["/api/canvas/{canvas_id}"]["GET"] |
| with TestClient(app) as client: |
| response = client.get("/api/canvas/test-canvas-id") |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 401, 403, 404] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_list_canvases_contracts(self): |
| """Test GET /api/canvas/ validates response schema.""" |
| if "/api/canvas/" in schema: |
| operation = schema["/api/canvas/"]["GET"] |
| with TestClient(app) as client: |
| response = client.get("/api/canvas/") |
| |
| operation.validate_response(response) |
| assert response.status_code in [200, 401, 403, 404] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_canvas_not_found(self): |
| """Test that 404 response conforms to schema.""" |
| if "/api/canvas/{canvas_id}" in schema: |
| with TestClient(app) as client: |
| |
| response = client.get("/api/canvas/nonexistent-canvas-999") |
| |
| assert response.status_code in [200, 401, 403, 404] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_canvas_list_pagination(self): |
| """Test that canvas list pagination conforms to schema.""" |
| if "/api/canvas/" in schema: |
| with TestClient(app) as client: |
| |
| response = client.get("/api/canvas/", params={"page": 1, "page_size": 10}) |
| assert response.status_code in [200, 401, 403, 404, 422] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_canvas_list_filtering(self): |
| """Test that canvas list filtering conforms to schema.""" |
| if "/api/canvas/" in schema: |
| with TestClient(app) as client: |
| |
| response = client.get("/api/canvas/", params={"canvas_type": "form"}) |
| assert response.status_code in [200, 401, 403, 404, 422] |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestCanvasTypeContracts: |
| """Contract tests for different canvas type schemas.""" |
|
|
| def test_chart_canvas_schema(self): |
| """Test that chart canvas response schema is valid.""" |
| |
| with TestClient(app) as client: |
| |
| |
| if "/api/canvas/{canvas_id}" in schema: |
| response = client.get("/api/canvas/chart-test") |
| |
| if response.status_code == 200: |
| json_resp = response.json() |
| |
| |
| pass |
|
|
| def test_form_canvas_schema(self): |
| """Test that form canvas with fields schema is valid.""" |
| |
| with TestClient(app) as client: |
| if "/api/canvas/{canvas_id}" in schema: |
| response = client.get("/api/canvas/form-test") |
| if response.status_code == 200: |
| json_resp = response.json() |
| |
| |
| pass |
|
|
| def test_markdown_canvas_schema(self): |
| """Test that markdown canvas content schema is valid.""" |
| |
| with TestClient(app) as client: |
| if "/api/canvas/{canvas_id}" in schema: |
| response = client.get("/api/canvas/markdown-test") |
| if response.status_code == 200: |
| json_resp = response.json() |
| |
| |
| pass |
|
|
| def test_sheet_canvas_schema(self): |
| """Test that spreadsheet data schema is valid.""" |
| |
| with TestClient(app) as client: |
| if "/api/canvas/{canvas_id}" in schema: |
| response = client.get("/api/canvas/sheet-test") |
| if response.status_code == 200: |
| json_resp = response.json() |
| |
| |
| pass |
|
|
| def test_table_canvas_schema(self): |
| """Test that table canvas schema is valid.""" |
| |
| with TestClient(app) as client: |
| if "/api/canvas/{canvas_id}" in schema: |
| response = client.get("/api/canvas/table-test") |
| if response.status_code == 200: |
| json_resp = response.json() |
| |
| |
| pass |
|
|
| def test_report_canvas_schema(self): |
| """Test that report canvas schema is valid.""" |
| |
| with TestClient(app) as client: |
| if "/api/canvas/{canvas_id}" in schema: |
| response = client.get("/api/canvas/report-test") |
| if response.status_code == 200: |
| json_resp = response.json() |
| |
| |
| pass |
|
|
| def test_alert_canvas_schema(self): |
| """Test that alert canvas schema is valid.""" |
| |
| with TestClient(app) as client: |
| if "/api/canvas/{canvas_id}" in schema: |
| response = client.get("/api/canvas/alert-test") |
| if response.status_code == 200: |
| json_resp = response.json() |
| |
| |
| pass |
|
|
|
|
| class TestCanvasUpdateContract: |
| """Contract tests for PUT /api/canvas/{id} endpoint.""" |
|
|
| def test_update_canvas_contracts(self): |
| """Test PUT /api/canvas/{id} validates request/response.""" |
| if "/api/canvas/{canvas_id}" in schema: |
| path_item = schema["/api/canvas/{canvas_id}"] |
| if "PUT" in path_item: |
| operation = path_item["PUT"] |
| with TestClient(app) as client: |
| response = client.put( |
| "/api/canvas/test-canvas", |
| json={"data": {}} |
| ) |
| operation.validate_response(response) |
| assert response.status_code in [200, 400, 401, 403, 404, 422] |
| else: |
| pytest.skip("PUT method not defined for this endpoint") |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_update_not_found(self): |
| """Test that updating non-existent canvas returns 404.""" |
| if "/api/canvas/{canvas_id}" in schema: |
| path_item = schema["/api/canvas/{canvas_id}"] |
| if "PUT" in path_item: |
| with TestClient(app) as client: |
| response = client.put( |
| "/api/canvas/nonexistent-canvas", |
| json={"data": {}} |
| ) |
| assert response.status_code in [200, 401, 403, 404, 422] |
| else: |
| pytest.skip("PUT method not defined for this endpoint") |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestCanvasDeleteContract: |
| """Contract tests for DELETE /api/canvas/{id} endpoint.""" |
|
|
| def test_delete_canvas_contracts(self): |
| """Test DELETE /api/canvas/{id} validates response.""" |
| if "/api/canvas/{canvas_id}" in schema: |
| path_item = schema["/api/canvas/{canvas_id}"] |
| if "DELETE" in path_item: |
| operation = path_item["DELETE"] |
| with TestClient(app) as client: |
| response = client.delete("/api/canvas/test-canvas") |
| operation.validate_response(response) |
| assert response.status_code in [200, 204, 401, 403, 404] |
| else: |
| pytest.skip("DELETE method not defined for this endpoint") |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
| def test_delete_not_found(self): |
| """Test that deleting non-existent canvas returns 404.""" |
| if "/api/canvas/{canvas_id}" in schema: |
| path_item = schema["/api/canvas/{canvas_id}"] |
| if "DELETE" in path_item: |
| with TestClient(app) as client: |
| response = client.delete("/api/canvas/nonexistent-canvas") |
| assert response.status_code in [200, 204, 401, 403, 404] |
| else: |
| pytest.skip("DELETE method not defined for this endpoint") |
| else: |
| pytest.skip("Endpoint not in OpenAPI schema") |
|
|
|
|
| class TestCanvasWebSocketContract: |
| """Contract tests for canvas WebSocket endpoints. |
| |
| Note: Schemathesis doesn't handle WebSocket connections. |
| These tests document WS endpoints for manual testing. |
| """ |
|
|
| def test_websocket_endpoints_documented(self): |
| """Test that WebSocket endpoints are documented in schema.""" |
| |
| schema = app.openapi() |
| paths = schema.get("paths", {}) |
|
|
| ws_endpoints = [] |
| for path, methods in paths.items(): |
| for method, details in methods.items(): |
| |
| if "ws" in str(details).lower() or "websocket" in str(details).lower(): |
| ws_endpoints.append(path) |
|
|
| |
| |
| |
| |
| pass |
|
|
| def test_websocket_auth_headers(self): |
| """Test that WS endpoints document auth requirements.""" |
| |
| |
| pass |
|
|
|
|
| class TestCanvasSpecificValidations: |
| """Custom validation tests for canvas-specific requirements.""" |
|
|
| def test_canvas_id_format(self): |
| """Test that canvas_id follows expected format.""" |
| |
| with TestClient(app) as client: |
| |
| test_ids = [ |
| "valid-canvas-id", |
| "ValidCanvas123", |
| "valid_canvas.id" |
| ] |
| for canvas_id in test_ids: |
| if "/api/canvas/{canvas_id}" in schema: |
| response = client.get(f"/api/canvas/{canvas_id}") |
| |
| assert response.status_code in [200, 401, 403, 404] |
|
|
| def test_form_data_structure(self): |
| """Test that form_data structure is validated.""" |
| if "/api/canvas/submit" in schema: |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/canvas/submit", |
| json={ |
| "canvas_id": "structure-test", |
| "form_data": { |
| "string_field": "value", |
| "number_field": 123, |
| "boolean_field": True, |
| "array_field": [1, 2, 3], |
| "object_field": {"nested": "data"} |
| } |
| } |
| ) |
| |
| assert response.status_code in [200, 400, 401, 403, 422] |
|
|
| def test_canvas_type_validation(self): |
| """Test that canvas_type parameter is validated.""" |
| |
| valid_types = ["chart", "form", "markdown", "sheet", "table", "report", "alert"] |
|
|
| with TestClient(app) as client: |
| for canvas_type in valid_types: |
| |
| if "/api/canvas/" in schema: |
| response = client.get("/api/canvas/", params={"canvas_type": canvas_type}) |
| assert response.status_code in [200, 401, 403, 404, 422] |
|
|