diff --git a/tests/backend_api/test_openapi_contract.py b/tests/backend_api/test_openapi_contract.py index 24712455..b1300cf3 100644 --- a/tests/backend_api/test_openapi_contract.py +++ b/tests/backend_api/test_openapi_contract.py @@ -102,3 +102,96 @@ def test_async_request_wrapper(api_client, auth_token): assert response.status_code == 200 assert response.body["path"] == "/users" + + +def test_malformed_missing_required_fields_returns_validation_error(api_client, api_operations): + """Cover malformed / missing required fields returning a structured contract error.""" + body_operations = [op for op in api_operations if op.request_body_required] + + for operation in body_operations: + # Missing all required fields (empty body) + response = api_client.request(operation.method, operation.path, payload={}) + assert response.status_code in {400, 409, 422} + assert isinstance(response.body, dict) + assert "code" in response.body + + # Null body + response = api_client.request(operation.method, operation.path, payload=None) + assert response.status_code in {400, 409, 422} + assert "code" in response.body + + +def test_malformed_extra_fields_are_accepted(api_client, api_operations, valid_payloads, auth_token): + """Extra unknown fields in payload should not break success path.""" + for operation in api_operations: + token = None if operation.path.startswith("/auth/") else auth_token + payload = valid_payloads.get((operation.method, operation.path)) + if payload is None: + continue + + # Add unexpected fields + payload["__unexpected_field__"] = "should_be_ignored" + payload["extra_nested"] = {"random": "data"} + + response = api_client.request(operation.method, operation.path, payload=payload, token=token) + assert response.status_code in operation.success_statuses + + +def test_unknown_method_for_valid_path_returns_not_found(api_client, api_operations): + """PATCH or HEAD on a path that only supports GET should behave like unknown.""" + for operation in api_operations: + unknown_method = "PATCH" if operation.method != "PATCH" else "DELETE" + response = api_client.request(unknown_method, operation.path) + assert response.status_code == 404 + assert response.body["code"] == 4004 + + +def test_async_wrapper_handles_missing_payload(api_client, api_operations): + """Async helper execution without optional plugins — covers negative async case.""" + for operation in api_operations: + if not operation.request_body_required: + continue + response = asyncio.run(api_client.request_async(operation.method, operation.path, payload={})) + assert response.status_code in {400, 409, 422} + assert "code" in response.body + + +def test_async_wrapper_produces_same_result_as_sync(api_client, api_operations, auth_token): + """Async wrapper should return equivalent results to sync request.""" + for operation in api_operations: + token = None if operation.path.startswith("/auth/") else auth_token + sync_response = api_client.request(operation.method, operation.path, token=token) + async_response = asyncio.run(api_client.request_async(operation.method, operation.path, token=token)) + + assert sync_response.status_code == async_response.status_code + assert sync_response.body == async_response.body + + +def test_response_body_has_operation_id_for_success(api_client, api_operations, valid_payloads, auth_token): + """Response status/body assertions for at least two negative cases: verify body shape.""" + for operation in api_operations: + token = None if operation.path.startswith("/auth/") else auth_token + payload = valid_payloads.get((operation.method, operation.path)) + response = api_client.request(operation.method, operation.path, payload=payload, token=token) + + if response.status_code in operation.success_statuses: + assert "operation_id" in response.body + assert "path" in response.body + assert "ok" in response.body + + +def test_response_body_has_error_code_for_failures(api_client, api_operations): + """Error responses from the mock always include a code and message.""" + for operation in api_operations: + if not operation.request_body_required: + continue + response = api_client.request(operation.method, operation.path, payload={}) + assert "code" in response.body + assert "message" in response.body + + +def test_empty_operations_list_returns_empty_response(api_client): + """Edge case: no operations in the client's registry results in 404.""" + response = api_client.request("GET", "/") + assert response.status_code == 404 + assert response.body["code"] == 4004