From 40df3c26c6a412465ea19eb43bceabb39a7dbf68 Mon Sep 17 00:00:00 2001 From: GareArc Date: Wed, 10 Jun 2026 03:04:43 -0700 Subject: [PATCH] test(openapi): pin error-path matrix to canonical wire codes Adds TestErrorMatrix (23 parametrized rows) covering every exception class raised or mapped in files.py and app_run.py, asserting the exact wire code each path emits and that every emitted code is an OpenApiErrorCode member. Also adds error_code = "filename_not_exists" to FilenameNotExistsError, which had no explicit code and was falling through to the status-map (bad_request). --- api/controllers/common/errors.py | 1 + .../openapi/test_error_contract.py | 74 ++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/api/controllers/common/errors.py b/api/controllers/common/errors.py index 31dd2fcd74..a8ab4d5bad 100644 --- a/api/controllers/common/errors.py +++ b/api/controllers/common/errors.py @@ -4,6 +4,7 @@ from libs.exception import BaseHTTPException class FilenameNotExistsError(HTTPException): + error_code = "filename_not_exists" code = 400 description = "The specified filename does not exist." diff --git a/api/tests/unit_tests/controllers/openapi/test_error_contract.py b/api/tests/unit_tests/controllers/openapi/test_error_contract.py index 684a123012..e3ae80ca6d 100644 --- a/api/tests/unit_tests/controllers/openapi/test_error_contract.py +++ b/api/tests/unit_tests/controllers/openapi/test_error_contract.py @@ -3,8 +3,25 @@ from unittest.mock import MagicMock, patch import pytest -from werkzeug.exceptions import Conflict, NotFound, UnprocessableEntity +from werkzeug.exceptions import ( + BadGateway, + BadRequest, + Conflict, + Forbidden, + InternalServerError, + NotFound, + Unauthorized, + UnprocessableEntity, +) +from controllers.common.errors import ( + BlockedFileExtensionError, + FilenameNotExistsError, + FileTooLargeError, + NoFileUploadedError, + TooManyFilesError, + UnsupportedFileTypeError, +) from controllers.openapi._errors import ( ErrorBody, ErrorDetail, @@ -14,7 +31,15 @@ from controllers.openapi._errors import ( OpenApiErrorCode, OpenApiErrorFormatter, ) -from controllers.web.error import ProviderQuotaExceededError +from controllers.service_api.app.error import ( + AppUnavailableError, + CompletionRequestError, + ConversationCompletedError, + ProviderModelCurrentlyNotSupportError, + ProviderNotInitializeError, + ProviderQuotaExceededError, +) +from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError class TestErrorBodyModel: @@ -258,3 +283,48 @@ class TestWireContract: assert resp.status_code == 400 wire = resp.get_json() assert wire == {"error": "expired_token"} + + +ERROR_MATRIX = [ + (BadRequest("x"), 400, "bad_request"), + (Unauthorized("x"), 401, "unauthorized"), + (Forbidden("x"), 403, "forbidden"), + (NotFound("x"), 404, "not_found"), + (Conflict("x"), 409, "conflict"), + (UnprocessableEntity("x"), 422, "invalid_param"), + (InternalServerError(), 500, "internal_server_error"), + (BadGateway("x"), 502, "bad_gateway"), + (AppUnavailableError(), 400, "app_unavailable"), + (ConversationCompletedError(), 400, "conversation_completed"), + (ProviderNotInitializeError(), 400, "provider_not_initialize"), + (ProviderQuotaExceededError(), 400, "provider_quota_exceeded"), + (ProviderModelCurrentlyNotSupportError(), 400, "model_currently_not_support"), + (CompletionRequestError(), 400, "completion_request_error"), + (InvokeRateLimitHttpError(), 429, "rate_limit_error"), + (FileTooLargeError(), 413, "file_too_large"), + (UnsupportedFileTypeError(), 415, "unsupported_file_type"), + (NoFileUploadedError(), 400, "no_file_uploaded"), + (TooManyFilesError(), 400, "too_many_files"), + (FilenameNotExistsError(), 400, "filename_not_exists"), + (BlockedFileExtensionError(), 400, "file_extension_blocked"), + (MemberLimitExceeded(), 403, "member_limit_exceeded"), + (MemberLicenseExceeded(), 403, "member_license_exceeded"), +] + + +class TestErrorMatrix: + @pytest.mark.parametrize( + ("exc", "status", "expected_code"), + ERROR_MATRIX, + ids=lambda v: type(v).__name__ if isinstance(v, Exception) else str(v), + ) + def test_every_known_error_path_yields_canonical_code(self, exc, status, expected_code): + fmt = OpenApiErrorFormatter() + data = dict(getattr(exc, "data", None) or {"message": str(exc), "status": status}) + + wire = fmt.finalize(exc, data, status) + + assert wire["code"] == expected_code + assert wire["status"] == status + assert wire["code"] in {c.value for c in OpenApiErrorCode} + ErrorBody.model_validate(wire)