diff --git a/src/langbot/pkg/api/http/controller/groups/user.py b/src/langbot/pkg/api/http/controller/groups/user.py index ee2ee7dc9..4ad52c7d4 100644 --- a/src/langbot/pkg/api/http/controller/groups/user.py +++ b/src/langbot/pkg/api/http/controller/groups/user.py @@ -111,7 +111,11 @@ class UserRouterGroup(group.RouterGroup): async def _() -> str: if getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud': return self.http_status(403, 'password_login_disabled', 'Password login is disabled on LangBot Cloud') - json_data = await quart.request.json + json_data = await quart.request.get_json(silent=True) + if not isinstance(json_data, dict) or not all( + isinstance(json_data.get(field), str) for field in ('user', 'password') + ): + return self.http_status(400, -1, 'Invalid request parameters') user_email = json_data['user'] password = json_data['password'] diff --git a/tests/integration/api/test_smoke.py b/tests/integration/api/test_smoke.py index 2b717937d..14ca4579f 100644 --- a/tests/integration/api/test_smoke.py +++ b/tests/integration/api/test_smoke.py @@ -231,33 +231,42 @@ class TestInvalidPayload: """Tests for error handling with invalid payloads.""" @pytest.mark.asyncio - async def test_missing_json_body(self, quart_test_client): - """ - POST endpoint without JSON body handles gracefully. - """ - # /api/v1/user/auth expects JSON with 'user' and 'password' + async def test_missing_json_body(self, quart_test_client, fake_api_app): + """A missing login body is rejected before authentication.""" response = await quart_test_client.post('/api/v1/user/auth') - # Should return error (500, 400, or 401) with stable JSON structure - assert response.status_code in (400, 500, 401) - data = await response.get_json() - - # Verify error response has expected structure - assert 'code' in data - assert 'msg' in data + assert response.status_code == 400 + assert await response.get_json() == {'code': -1, 'msg': 'Invalid request parameters'} + fake_api_app.user_service.authenticate.assert_not_awaited() @pytest.mark.asyncio - async def test_invalid_json_structure(self, quart_test_client): - """ - POST with wrong JSON structure returns stable error. - """ - response = await quart_test_client.post('/api/v1/user/auth', json={'wrong_field': 'value'}) + @pytest.mark.parametrize( + 'payload', + [ + {'wrong_field': 'value'}, + [], + {'user': None, 'password': 'secret'}, + {'user': 'owner@example.com', 'password': 42}, + ], + ) + async def test_invalid_json_structure(self, quart_test_client, fake_api_app, payload): + """Malformed credential fields never reach the authentication service.""" + response = await quart_test_client.post('/api/v1/user/auth', json=payload) - # Should return error with stable JSON structure - assert response.status_code in (400, 500, 401) - data = await response.get_json() - assert 'code' in data - assert 'msg' in data + assert response.status_code == 400 + assert await response.get_json() == {'code': -1, 'msg': 'Invalid request parameters'} + fake_api_app.user_service.authenticate.assert_not_awaited() + + @pytest.mark.asyncio + @pytest.mark.parametrize('password', ['', 'existing-password']) + async def test_string_credentials_reach_authentication(self, quart_test_client, fake_api_app, password): + response = await quart_test_client.post( + '/api/v1/user/auth', json={'user': 'owner@example.com', 'password': password} + ) + + assert response.status_code == 200 + assert (await response.get_json())['data']['token'] == 'fake_token' + fake_api_app.user_service.authenticate.assert_awaited_once_with('owner@example.com', password, None) @pytest.mark.usefixtures('mock_circular_import_chain')