From 3a6901e718f91c85326bd6993b70a36430fd94b9 Mon Sep 17 00:00:00 2001 From: GareArc Date: Tue, 5 May 2026 20:08:43 -0700 Subject: [PATCH] fix(openapi): /apps 422 body emits JSON ValidationError -> UnprocessableEntity(exc.json()) so CLI consumers can parse the error body. The previous str(errors()) produced a Python repr (single-quoted dicts), not JSON. Also align with sibling openapi controllers: request.args.to_dict(flat=True) and 'as exc' naming. Test cleanup: hoist module-scope imports; add a happy-path positive case covering every field. --- api/controllers/openapi/apps.py | 6 +-- .../openapi/test_app_list_query.py | 43 +++++++++++-------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index 38b6c0bbd3..6396d162fe 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -178,9 +178,9 @@ class AppListApi(Resource): return PaginationEnvelope[AppListRow].build(page=1, limit=0, total=0, items=[]).model_dump(mode="json"), 200 try: - query = AppListQuery.model_validate(dict(request.args)) - except ValidationError as e: - raise UnprocessableEntity(str(e.errors())) + query = AppListQuery.model_validate(request.args.to_dict(flat=True)) + except ValidationError as exc: + raise UnprocessableEntity(exc.json()) workspace_id = query.workspace_id require_workspace_member(ctx, workspace_id) diff --git a/api/tests/unit_tests/controllers/openapi/test_app_list_query.py b/api/tests/unit_tests/controllers/openapi/test_app_list_query.py index c60eacb3c9..f7e8e9c73a 100644 --- a/api/tests/unit_tests/controllers/openapi/test_app_list_query.py +++ b/api/tests/unit_tests/controllers/openapi/test_app_list_query.py @@ -13,10 +13,11 @@ from __future__ import annotations import pytest from pydantic import ValidationError +from controllers.openapi._models import MAX_PAGE_LIMIT +from controllers.openapi.apps import AppListQuery + def test_defaults(): - from controllers.openapi.apps import AppListQuery - q = AppListQuery.model_validate({"workspace_id": "ws-1"}) assert q.workspace_id == "ws-1" assert q.page == 1 @@ -27,15 +28,11 @@ def test_defaults(): def test_workspace_id_required(): - from controllers.openapi.apps import AppListQuery - with pytest.raises(ValidationError): AppListQuery.model_validate({}) def test_page_must_be_positive(): - from controllers.openapi.apps import AppListQuery - with pytest.raises(ValidationError): AppListQuery.model_validate({"workspace_id": "ws-1", "page": 0}) with pytest.raises(ValidationError): @@ -43,15 +40,11 @@ def test_page_must_be_positive(): def test_page_rejects_non_integer_string(): - from controllers.openapi.apps import AppListQuery - with pytest.raises(ValidationError): AppListQuery.model_validate({"workspace_id": "ws-1", "page": "abc"}) def test_limit_must_be_positive(): - from controllers.openapi.apps import AppListQuery - with pytest.raises(ValidationError): AppListQuery.model_validate({"workspace_id": "ws-1", "limit": 0}) with pytest.raises(ValidationError): @@ -59,9 +52,6 @@ def test_limit_must_be_positive(): def test_limit_caps_at_max_page_limit(): - from controllers.openapi._models import MAX_PAGE_LIMIT - from controllers.openapi.apps import AppListQuery - # Boundary accepts. q = AppListQuery.model_validate({"workspace_id": "ws-1", "limit": MAX_PAGE_LIMIT}) assert q.limit == MAX_PAGE_LIMIT @@ -72,8 +62,6 @@ def test_limit_caps_at_max_page_limit(): def test_mode_whitelisted_against_app_mode(): - from controllers.openapi.apps import AppListQuery - # Valid mode passes. q = AppListQuery.model_validate({"workspace_id": "ws-1", "mode": "chat"}) assert q.mode is not None @@ -85,16 +73,33 @@ def test_mode_whitelisted_against_app_mode(): def test_name_length_capped(): - from controllers.openapi.apps import AppListQuery - AppListQuery.model_validate({"workspace_id": "ws-1", "name": "x" * 200}) with pytest.raises(ValidationError): AppListQuery.model_validate({"workspace_id": "ws-1", "name": "x" * 201}) def test_tag_length_capped(): - from controllers.openapi.apps import AppListQuery - AppListQuery.model_validate({"workspace_id": "ws-1", "tag": "x" * 100}) with pytest.raises(ValidationError): AppListQuery.model_validate({"workspace_id": "ws-1", "tag": "x" * 101}) + + +def test_all_fields_accept_valid_values(): + """Pin the happy-path acceptance for every field in one place.""" + q = AppListQuery.model_validate( + { + "workspace_id": "ws-1", + "page": 5, + "limit": 50, + "mode": "workflow", + "name": "search", + "tag": "prod", + } + ) + assert q.workspace_id == "ws-1" + assert q.page == 5 + assert q.limit == 50 + assert q.mode is not None + assert q.mode.value == "workflow" + assert q.name == "search" + assert q.tag == "prod"