diff --git a/routes/auth_routes.py b/routes/auth_routes.py index 1042437ac..378f22afa 100644 --- a/routes/auth_routes.py +++ b/routes/auth_routes.py @@ -887,6 +887,8 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter: """Create a new integration (admin only).""" _require_admin_user(request) body = await request.json() + if not isinstance(body, dict): + raise HTTPException(400, "Request body must be a JSON object") item = add_integration(body) return {"ok": True, "integration": mask_integration_secret(item)} @@ -895,6 +897,8 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter: """Update an existing integration (admin only).""" _require_admin_user(request) body = await request.json() + if not isinstance(body, dict): + raise HTTPException(400, "Request body must be a JSON object") item = update_integration(integration_id, body) if not item: raise HTTPException(404, "Integration not found") diff --git a/routes/document/document_routes.py b/routes/document/document_routes.py index 2b987e140..2a820de75 100644 --- a/routes/document/document_routes.py +++ b/routes/document/document_routes.py @@ -781,9 +781,11 @@ def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: except Exception as e: logger.warning("Failed to parse export request body, defaulting to empty", exc_info=e) data = {} - ids = data.get("ids") or [] + ids = data.get("ids") if isinstance(data, dict) else None if not ids: raise HTTPException(400, "No documents specified") + if not isinstance(ids, list) or not all(isinstance(i, str) for i in ids): + raise HTTPException(400, "ids must be a list of document ids") _ext = { "javascript": ".js", "python": ".py", "html": ".html", "css": ".css", "markdown": ".md", "json": ".json", "yaml": ".yml", "bash": ".sh", diff --git a/tests/test_document_export_zip_ids.py b/tests/test_document_export_zip_ids.py new file mode 100644 index 000000000..3a2510d1c --- /dev/null +++ b/tests/test_document_export_zip_ids.py @@ -0,0 +1,47 @@ +"""POST /api/documents/export-zip answers 400 for ids that are not a list of document ids.""" + +import json +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest +from fastapi import HTTPException + +from tests.helpers.import_state import clear_fake_database_modules + +clear_fake_database_modules() + +import routes.document_routes as droutes + + +class _Req: + def __init__(self, body): + self.state = SimpleNamespace(current_user="tester") + self._body = body + + async def json(self): + return self._body + + +def _export_zip(): + router = droutes.setup_document_routes(MagicMock(), None) + for r in router.routes: + if getattr(r, "path", None) == "/api/documents/export-zip": + return r.endpoint + raise RuntimeError("export-zip route not found") + + +@pytest.mark.parametrize("body", [[], "x", 5, {}, {"ids": []}, {"ids": None}]) +async def test_missing_ids_is_a_400(body): + with pytest.raises(HTTPException) as exc: + await _export_zip()(_Req(body)) + assert exc.value.status_code == 400 + assert exc.value.detail == "No documents specified" + + +@pytest.mark.parametrize("ids", [5, "abc", {"a": 1}, [1, 2], [["a"]], ["a", None]]) +async def test_ids_that_are_not_a_list_of_strings_is_a_400(ids): + with pytest.raises(HTTPException) as exc: + await _export_zip()(_Req({"ids": ids})) + assert exc.value.status_code == 400 + assert exc.value.detail == "ids must be a list of document ids" diff --git a/tests/test_integrations_store_shape.py b/tests/test_integrations_store_shape.py index c97f71b5e..c93e5324d 100644 --- a/tests/test_integrations_store_shape.py +++ b/tests/test_integrations_store_shape.py @@ -177,3 +177,28 @@ def test_update_integration_rejects_invalid_base_url_without_changing_existing( assert exc.value.status_code == 400 assert exc.value.detail == message assert integrations.load_integrations()[0]["base_url"] == "https://example.test" + + +@pytest.mark.parametrize("body", [[], "x", 5, None]) +def test_create_integration_rejects_non_object_body(integrations_routes, body): + endpoint, session_cookie, http_exception = integrations_routes + create_integration = endpoint("/api/auth/integrations", "POST") + + with pytest.raises(http_exception) as exc: + asyncio.run(create_integration(_JsonRequest(body, session_cookie))) + + assert exc.value.status_code == 400 + assert integrations.load_integrations() == [] + + +@pytest.mark.parametrize("body", [[], "x", 5, None]) +def test_update_integration_rejects_non_object_body(integrations_routes, body): + endpoint, session_cookie, http_exception = integrations_routes + update_integration = endpoint("/api/auth/integrations/{integration_id}", "PUT") + integrations.save_integrations([{"id": "existing", "name": "Original", "base_url": "https://example.test"}]) + + with pytest.raises(http_exception) as exc: + asyncio.run(update_integration("existing", _JsonRequest(body, session_cookie))) + + assert exc.value.status_code == 400 + assert integrations.load_integrations()[0]["name"] == "Original"