diff --git a/tests/integration/api/test_knowledge.py b/tests/integration/api/test_knowledge.py index 13b0785c7..a046da6d4 100644 --- a/tests/integration/api/test_knowledge.py +++ b/tests/integration/api/test_knowledge.py @@ -117,13 +117,13 @@ def fake_knowledge_app(): 'engine_plugin_id': 'test/engine', } ) - app.knowledge_service.create_knowledge_base = AsyncMock(return_value={'uuid': 'new-kb-uuid'}) + app.knowledge_service.create_knowledge_base = AsyncMock(return_value='new-kb-uuid') app.knowledge_service.update_knowledge_base = AsyncMock(return_value={}) app.knowledge_service.delete_knowledge_base = AsyncMock() app.knowledge_service.get_files_by_knowledge_base = AsyncMock( return_value=[{'uuid': 'test-file-uuid', 'filename': 'test.pdf'}] ) - app.knowledge_service.store_file = AsyncMock(return_value={'task_id': 'test-task-id'}) + app.knowledge_service.store_file = AsyncMock(return_value='test-task-id') app.knowledge_service.delete_file = AsyncMock() app.knowledge_service.retrieve_knowledge_base = AsyncMock(return_value=[{'content': 'test result', 'score': 0.95}]) @@ -133,6 +133,11 @@ def fake_knowledge_app(): return app +@pytest.fixture(autouse=True) +def reset_service_calls(fake_knowledge_app): + fake_knowledge_app.knowledge_service.reset_mock() + + @pytest.fixture(scope='module') async def quart_test_client(fake_knowledge_app, http_controller_cls): """Create Quart test client (module scope to avoid route re-registration).""" @@ -148,7 +153,7 @@ class TestKnowledgeBaseEndpoints: """Tests for /api/v1/knowledge/bases endpoints.""" @pytest.mark.asyncio - async def test_get_knowledge_bases_success(self, quart_test_client): + async def test_get_knowledge_bases_success(self, quart_test_client, fake_knowledge_app): """GET /api/v1/knowledge/bases returns knowledge base list.""" response = await quart_test_client.get( '/api/v1/knowledge/bases', headers={'Authorization': 'Bearer test_token'} @@ -160,8 +165,24 @@ class TestKnowledgeBaseEndpoints: assert 'data' in data assert 'bases' in data['data'] + called = fake_knowledge_app.knowledge_service.get_knowledge_bases + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == () + assert called.await_args.kwargs == {'include_secret': True} + assert data['data']['bases'] == [ + { + 'uuid': 'test-kb-uuid', + 'name': 'Test Knowledge Base', + 'description': 'Test KB description', + 'engine_plugin_id': 'test/engine', + 'created_at': '2024-01-01T00:00:00', + 'updated_at': '2024-01-01T00:00:00', + } + ] + @pytest.mark.asyncio - async def test_create_knowledge_base_success(self, quart_test_client): + async def test_create_knowledge_base_success(self, quart_test_client, fake_knowledge_app): """POST /api/v1/knowledge/bases creates new knowledge base.""" response = await quart_test_client.post( '/api/v1/knowledge/bases', @@ -174,8 +195,15 @@ class TestKnowledgeBaseEndpoints: assert data['code'] == 0 assert 'uuid' in data['data'] + called = fake_knowledge_app.knowledge_service.create_knowledge_base + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == ({'name': 'New KB', 'engine_plugin_id': 'test/engine'},) + assert called.await_args.kwargs == {} + assert data['data'] == {'uuid': 'new-kb-uuid'} + @pytest.mark.asyncio - async def test_get_single_knowledge_base_success(self, quart_test_client): + async def test_get_single_knowledge_base_success(self, quart_test_client, fake_knowledge_app): """GET /api/v1/knowledge/bases/{uuid} returns knowledge base.""" response = await quart_test_client.get( '/api/v1/knowledge/bases/test-kb-uuid', headers={'Authorization': 'Bearer test_token'} @@ -186,8 +214,20 @@ class TestKnowledgeBaseEndpoints: assert data['code'] == 0 assert 'base' in data['data'] + called = fake_knowledge_app.knowledge_service.get_knowledge_base + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == ('test-kb-uuid',) + assert called.await_args.kwargs == {'include_secret': True} + assert data['data']['base'] == { + 'uuid': 'test-kb-uuid', + 'name': 'Test Knowledge Base', + 'description': 'Test KB description', + 'engine_plugin_id': 'test/engine', + } + @pytest.mark.asyncio - async def test_update_knowledge_base_success(self, quart_test_client): + async def test_update_knowledge_base_success(self, quart_test_client, fake_knowledge_app): """PUT /api/v1/knowledge/bases/{uuid} updates knowledge base.""" response = await quart_test_client.put( '/api/v1/knowledge/bases/test-kb-uuid', @@ -199,8 +239,14 @@ class TestKnowledgeBaseEndpoints: data = await response.get_json() assert data['code'] == 0 + called = fake_knowledge_app.knowledge_service.update_knowledge_base + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == ('test-kb-uuid', {'name': 'Updated KB'}) + assert called.await_args.kwargs == {} + @pytest.mark.asyncio - async def test_delete_knowledge_base_success(self, quart_test_client): + async def test_delete_knowledge_base_success(self, quart_test_client, fake_knowledge_app): """DELETE /api/v1/knowledge/bases/{uuid} deletes knowledge base.""" response = await quart_test_client.delete( '/api/v1/knowledge/bases/test-kb-uuid', headers={'Authorization': 'Bearer test_token'} @@ -208,13 +254,20 @@ class TestKnowledgeBaseEndpoints: assert response.status_code == 200 + called = fake_knowledge_app.knowledge_service.delete_knowledge_base + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == ('test-kb-uuid',) + assert called.await_args.kwargs == {} + assert (await response.get_json())['code'] == 0 + @pytest.mark.usefixtures('mock_circular_import_chain') class TestKnowledgeBaseFilesEndpoints: """Tests for knowledge base files endpoints.""" @pytest.mark.asyncio - async def test_get_files_success(self, quart_test_client): + async def test_get_files_success(self, quart_test_client, fake_knowledge_app): """GET /api/v1/knowledge/bases/{uuid}/files returns files.""" response = await quart_test_client.get( '/api/v1/knowledge/bases/test-kb-uuid/files', headers={'Authorization': 'Bearer test_token'} @@ -225,8 +278,15 @@ class TestKnowledgeBaseFilesEndpoints: assert data['code'] == 0 assert 'files' in data['data'] + called = fake_knowledge_app.knowledge_service.get_files_by_knowledge_base + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == ('test-kb-uuid',) + assert called.await_args.kwargs == {} + assert data['data']['files'] == [{'uuid': 'test-file-uuid', 'filename': 'test.pdf'}] + @pytest.mark.asyncio - async def test_add_file_to_knowledge_base(self, quart_test_client): + async def test_add_file_to_knowledge_base(self, quart_test_client, fake_knowledge_app): """POST /api/v1/knowledge/bases/{uuid}/files adds file.""" response = await quart_test_client.post( '/api/v1/knowledge/bases/test-kb-uuid/files', @@ -239,8 +299,15 @@ class TestKnowledgeBaseFilesEndpoints: assert data['code'] == 0 assert 'task_id' in data['data'] + called = fake_knowledge_app.knowledge_service.store_file + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == ('test-kb-uuid', 'test-file-id') + assert called.await_args.kwargs == {'parser_plugin_id': 'test/parser'} + assert data['data'] == {'task_id': 'test-task-id'} + @pytest.mark.asyncio - async def test_delete_file_from_knowledge_base(self, quart_test_client): + async def test_delete_file_from_knowledge_base(self, quart_test_client, fake_knowledge_app): """DELETE /api/v1/knowledge/bases/{uuid}/files/{file_id}.""" response = await quart_test_client.delete( '/api/v1/knowledge/bases/test-kb-uuid/files/test-file-uuid', headers={'Authorization': 'Bearer test_token'} @@ -248,13 +315,20 @@ class TestKnowledgeBaseFilesEndpoints: assert response.status_code == 200 + called = fake_knowledge_app.knowledge_service.delete_file + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == ('test-kb-uuid', 'test-file-uuid') + assert called.await_args.kwargs == {} + assert (await response.get_json())['code'] == 0 + @pytest.mark.usefixtures('mock_circular_import_chain') class TestKnowledgeBaseRetrieveEndpoint: """Tests for knowledge base retrieval endpoint.""" @pytest.mark.asyncio - async def test_retrieve_knowledge_success(self, quart_test_client): + async def test_retrieve_knowledge_success(self, quart_test_client, fake_knowledge_app): """POST /api/v1/knowledge/bases/{uuid}/retrieve.""" response = await quart_test_client.post( '/api/v1/knowledge/bases/test-kb-uuid/retrieve', @@ -267,6 +341,13 @@ class TestKnowledgeBaseRetrieveEndpoint: assert data['code'] == 0 assert 'results' in data['data'] + called = fake_knowledge_app.knowledge_service.retrieve_knowledge_base + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == '00000000-0000-0000-0000-00000000000a' + assert called.await_args.args[1:] == ('test-kb-uuid', 'test query', {'top_k': 5}) + assert called.await_args.kwargs == {} + assert data['data']['results'] == [{'content': 'test result', 'score': 0.95}] + @pytest.mark.asyncio async def test_retrieve_without_query_returns_error(self, quart_test_client): """POST retrieve without query returns 400.""" diff --git a/tests/integration/api/test_pipelines.py b/tests/integration/api/test_pipelines.py index 9f84aafb9..5e06388c8 100644 --- a/tests/integration/api/test_pipelines.py +++ b/tests/integration/api/test_pipelines.py @@ -128,10 +128,10 @@ def fake_pipeline_app(): 'config': {}, } ) - app.pipeline_service.create_pipeline = AsyncMock(return_value={'uuid': 'new-pipeline-uuid'}) + app.pipeline_service.create_pipeline = AsyncMock(return_value='new-pipeline-uuid') app.pipeline_service.update_pipeline = AsyncMock(return_value={}) app.pipeline_service.delete_pipeline = AsyncMock() - app.pipeline_service.copy_pipeline = AsyncMock(return_value={'uuid': 'copied-pipeline-uuid'}) + app.pipeline_service.copy_pipeline = AsyncMock(return_value='copied-pipeline-uuid') # Bot service app.bot_service = Mock() @@ -161,6 +161,11 @@ def fake_pipeline_app(): return app +@pytest.fixture(autouse=True) +def reset_service_calls(fake_pipeline_app): + fake_pipeline_app.pipeline_service.reset_mock() + + @pytest.fixture(scope='module') async def quart_test_client(fake_pipeline_app, http_controller_cls): """Create Quart test client (module scope to avoid route re-registration).""" @@ -179,7 +184,7 @@ class TestPipelineMetadataEndpoint: """Tests for /api/v1/pipelines/_/metadata endpoint.""" @pytest.mark.asyncio - async def test_get_pipeline_metadata_success(self, quart_test_client): + async def test_get_pipeline_metadata_success(self, quart_test_client, fake_pipeline_app): """GET /api/v1/pipelines/_/metadata returns metadata list.""" response = await quart_test_client.get( '/api/v1/pipelines/_/metadata', headers={'Authorization': 'Bearer test_token'} @@ -191,6 +196,13 @@ class TestPipelineMetadataEndpoint: assert 'data' in data assert isinstance(data['data'], dict) + called = fake_pipeline_app.pipeline_service.get_pipeline_metadata + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == () + assert called.await_args.kwargs == {} + assert data['data'] == {'configs': [{'name': 'trigger', 'stages': []}, {'name': 'ai', 'stages': []}]} + @pytest.mark.asyncio async def test_get_pipeline_metadata_requires_auth(self, quart_test_client): """Pipeline metadata endpoint requires authentication.""" @@ -203,7 +215,7 @@ class TestPipelinesListEndpoint: """Tests for /api/v1/pipelines endpoint.""" @pytest.mark.asyncio - async def test_get_pipelines_success(self, quart_test_client): + async def test_get_pipelines_success(self, quart_test_client, fake_pipeline_app): """GET /api/v1/pipelines returns pipeline list.""" response = await quart_test_client.get('/api/v1/pipelines', headers={'Authorization': 'Bearer test_token'}) @@ -212,24 +224,46 @@ class TestPipelinesListEndpoint: assert data['code'] == 0 assert 'data' in data + called = fake_pipeline_app.pipeline_service.get_pipelines + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('created_at', 'DESC') + assert called.await_args.kwargs == {'include_secret': True} + assert data['data']['pipelines'] == [ + { + 'uuid': 'test-pipeline-uuid', + 'name': 'Test Pipeline', + 'description': 'Test description', + 'created_at': '2024-01-01T00:00:00', + 'updated_at': '2024-01-01T00:00:00', + 'is_default': False, + } + ] + @pytest.mark.asyncio - async def test_get_pipelines_with_sort_param(self, quart_test_client): + async def test_get_pipelines_with_sort_param(self, quart_test_client, fake_pipeline_app): """GET pipelines with sort parameter.""" response = await quart_test_client.get( - '/api/v1/pipelines?sort_by=created_at&sort_order=DESC', headers={'Authorization': 'Bearer test_token'} + '/api/v1/pipelines?sort_by=name&sort_order=ASC', headers={'Authorization': 'Bearer test_token'} ) assert response.status_code == 200 data = await response.get_json() assert data['code'] == 0 + called = fake_pipeline_app.pipeline_service.get_pipelines + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('name', 'ASC') + assert called.await_args.kwargs == {'include_secret': True} + @pytest.mark.usefixtures('mock_circular_import_chain') class TestPipelinesCRUDEndpoints: """Tests for pipeline CRUD operations.""" @pytest.mark.asyncio - async def test_get_single_pipeline_success(self, quart_test_client): + async def test_get_single_pipeline_success(self, quart_test_client, fake_pipeline_app): """GET /api/v1/pipelines/{uuid} returns pipeline.""" response = await quart_test_client.get( '/api/v1/pipelines/test-pipeline-uuid', headers={'Authorization': 'Bearer test_token'} @@ -240,8 +274,15 @@ class TestPipelinesCRUDEndpoints: assert data['code'] == 0 assert 'data' in data + called = fake_pipeline_app.pipeline_service.get_pipeline + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-pipeline-uuid',) + assert called.await_args.kwargs == {'include_secret': True} + assert data['data']['pipeline'] == {'uuid': 'test-pipeline-uuid', 'name': 'Test Pipeline', 'config': {}} + @pytest.mark.asyncio - async def test_create_pipeline_success(self, quart_test_client): + async def test_create_pipeline_success(self, quart_test_client, fake_pipeline_app): """POST /api/v1/pipelines creates new pipeline.""" response = await quart_test_client.post( '/api/v1/pipelines', @@ -254,6 +295,13 @@ class TestPipelinesCRUDEndpoints: assert data['code'] == 0 assert 'uuid' in data['data'] + called = fake_pipeline_app.pipeline_service.create_pipeline + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ({'name': 'New Pipeline', 'config': {}},) + assert called.await_args.kwargs == {'default': False} + assert data['data'] == {'uuid': 'new-pipeline-uuid'} + @pytest.mark.asyncio async def test_create_default_pipeline_forwards_default_flag(self, quart_test_client, fake_pipeline_app): """POST /api/v1/pipelines explicitly creates a default pipeline.""" @@ -271,7 +319,7 @@ class TestPipelinesCRUDEndpoints: assert call.args[1]['is_default'] is True @pytest.mark.asyncio - async def test_update_pipeline_success(self, quart_test_client): + async def test_update_pipeline_success(self, quart_test_client, fake_pipeline_app): """PUT /api/v1/pipelines/{uuid} updates pipeline.""" response = await quart_test_client.put( '/api/v1/pipelines/test-pipeline-uuid', @@ -283,8 +331,14 @@ class TestPipelinesCRUDEndpoints: data = await response.get_json() assert data['code'] == 0 + called = fake_pipeline_app.pipeline_service.update_pipeline + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-pipeline-uuid', {'name': 'Updated Pipeline'}) + assert called.await_args.kwargs == {} + @pytest.mark.asyncio - async def test_delete_pipeline_success(self, quart_test_client): + async def test_delete_pipeline_success(self, quart_test_client, fake_pipeline_app): """DELETE /api/v1/pipelines/{uuid} deletes pipeline.""" response = await quart_test_client.delete( '/api/v1/pipelines/test-pipeline-uuid', headers={'Authorization': 'Bearer test_token'} @@ -294,8 +348,14 @@ class TestPipelinesCRUDEndpoints: data = await response.get_json() assert data['code'] == 0 + called = fake_pipeline_app.pipeline_service.delete_pipeline + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-pipeline-uuid',) + assert called.await_args.kwargs == {} + @pytest.mark.asyncio - async def test_copy_pipeline_success(self, quart_test_client): + async def test_copy_pipeline_success(self, quart_test_client, fake_pipeline_app): """POST /api/v1/pipelines/{uuid}/copy copies pipeline.""" response = await quart_test_client.post( '/api/v1/pipelines/test-pipeline-uuid/copy', headers={'Authorization': 'Bearer test_token'} @@ -306,14 +366,24 @@ class TestPipelinesCRUDEndpoints: assert data['code'] == 0 assert 'uuid' in data['data'] + called = fake_pipeline_app.pipeline_service.copy_pipeline + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-pipeline-uuid',) + assert called.await_args.kwargs == {} + assert data['data'] == {'uuid': 'copied-pipeline-uuid'} + @pytest.mark.usefixtures('mock_circular_import_chain') class TestPipelineExtensionsEndpoint: """Tests for pipeline extensions.""" @pytest.mark.asyncio - async def test_get_extensions(self, quart_test_client): + async def test_get_extensions(self, quart_test_client, fake_pipeline_app): """GET /api/v1/pipelines/{uuid}/extensions.""" + fake_pipeline_app.plugin_connector.list_plugins.return_value = [{'name': 'extension-plugin'}] + fake_pipeline_app.mcp_service.get_mcp_servers.return_value = [{'uuid': 'extension-mcp'}] + fake_pipeline_app.skill_service.list_skills.return_value = [{'name': 'extension-skill'}] response = await quart_test_client.get( '/api/v1/pipelines/test-pipeline-uuid/extensions', headers={'Authorization': 'Bearer test_token'} ) @@ -323,6 +393,21 @@ class TestPipelineExtensionsEndpoint: data = await response.get_json() assert data['code'] == 0 + called = fake_pipeline_app.pipeline_service.get_pipeline + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-pipeline-uuid',) + assert called.await_args.kwargs == {} + assert data['data']['available_plugins'] == [{'name': 'extension-plugin'}] + assert data['data']['available_mcp_servers'] == [{'uuid': 'extension-mcp'}] + assert data['data']['available_skills'] == [{'name': 'extension-skill'}] + assert data['data']['bound_plugins'] == [] + assert data['data']['bound_mcp_servers'] == [] + assert data['data']['bound_skills'] == [] + assert data['data']['enable_all_plugins'] is False + assert data['data']['enable_all_mcp_servers'] is False + assert data['data']['enable_all_skills'] is False + @pytest.mark.asyncio async def test_get_extensions_redacts_available_plugin_secrets( self, diff --git a/tests/integration/api/test_providers.py b/tests/integration/api/test_providers.py index bedd1d871..cc01466b4 100644 --- a/tests/integration/api/test_providers.py +++ b/tests/integration/api/test_providers.py @@ -109,21 +109,23 @@ def fake_provider_app(): app.llm_model_service = Mock() app.llm_model_service.get_llm_models = AsyncMock(return_value=[{'uuid': 'test-model-uuid', 'name': 'gpt-4'}]) app.llm_model_service.get_llm_model = AsyncMock(return_value={'uuid': 'test-model-uuid', 'name': 'gpt-4'}) - app.llm_model_service.create_llm_model = AsyncMock(return_value={'uuid': 'new-model-uuid'}) + app.llm_model_service.create_llm_model = AsyncMock(return_value='new-model-uuid') app.llm_model_service.update_llm_model = AsyncMock(return_value={}) app.llm_model_service.delete_llm_model = AsyncMock() app.llm_model_service.test_llm_model = AsyncMock() # Embedding model service app.embedding_models_service = Mock() - app.embedding_models_service.get_embedding_models = AsyncMock(return_value=[]) - app.embedding_models_service.create_embedding_model = AsyncMock(return_value={'uuid': 'new-embedding-uuid'}) + app.embedding_models_service.get_embedding_models = AsyncMock( + return_value=[{'uuid': 'embedding-model', 'name': 'Embedding'}] + ) + app.embedding_models_service.create_embedding_model = AsyncMock(return_value='new-embedding-uuid') app.embedding_models_service.test_embedding_model = AsyncMock() # Rerank model service app.rerank_models_service = Mock() - app.rerank_models_service.get_rerank_models = AsyncMock(return_value=[]) - app.rerank_models_service.create_rerank_model = AsyncMock(return_value={'uuid': 'new-rerank-uuid'}) + app.rerank_models_service.get_rerank_models = AsyncMock(return_value=[{'uuid': 'rerank-model', 'name': 'Rerank'}]) + app.rerank_models_service.create_rerank_model = AsyncMock(return_value='new-rerank-uuid') app.rerank_models_service.test_rerank_model = AsyncMock() # Model manager @@ -134,6 +136,14 @@ def fake_provider_app(): return app +@pytest.fixture(autouse=True) +def reset_service_calls(fake_provider_app): + fake_provider_app.provider_service.reset_mock() + fake_provider_app.llm_model_service.reset_mock() + fake_provider_app.embedding_models_service.reset_mock() + fake_provider_app.rerank_models_service.reset_mock() + + @pytest.fixture(scope='module') async def quart_test_client(fake_provider_app, http_controller_cls): """Create Quart test client (module scope to avoid route re-registration).""" @@ -206,7 +216,7 @@ class TestProviderEndpoints: assert data['data']['uuid'] == 'new-provider-uuid' @pytest.mark.asyncio - async def test_update_provider_success(self, quart_test_client): + async def test_update_provider_success(self, quart_test_client, fake_provider_app): """PUT /api/v1/provider/providers/{uuid} updates provider.""" response = await quart_test_client.put( '/api/v1/provider/providers/test-provider-uuid', @@ -218,8 +228,14 @@ class TestProviderEndpoints: data = await response.get_json() assert data['code'] == 0 + called = fake_provider_app.provider_service.update_provider + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-provider-uuid', {'name': 'Updated Provider'}) + assert called.await_args.kwargs == {} + @pytest.mark.asyncio - async def test_delete_provider_success(self, quart_test_client): + async def test_delete_provider_success(self, quart_test_client, fake_provider_app): """DELETE /api/v1/provider/providers/{uuid} deletes provider.""" response = await quart_test_client.delete( '/api/v1/provider/providers/test-provider-uuid', headers={'Authorization': 'Bearer test_token'} @@ -227,8 +243,15 @@ class TestProviderEndpoints: assert response.status_code == 200 + called = fake_provider_app.provider_service.delete_provider + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-provider-uuid',) + assert called.await_args.kwargs == {} + assert (await response.get_json())['code'] == 0 + @pytest.mark.asyncio - async def test_get_provider_includes_model_counts(self, quart_test_client): + async def test_get_provider_includes_model_counts(self, quart_test_client, fake_provider_app): """GET provider response includes model counts.""" response = await quart_test_client.get( '/api/v1/provider/providers/test-provider-uuid', headers={'Authorization': 'Bearer test_token'} @@ -243,13 +266,24 @@ class TestProviderEndpoints: assert 'embedding_count' in provider_data assert 'rerank_count' in provider_data + called = fake_provider_app.provider_service.get_provider_model_counts + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-provider-uuid',) + assert called.await_args.kwargs == {} + assert {key: provider_data[key] for key in ('llm_count', 'embedding_count', 'rerank_count')} == { + 'llm_count': 2, + 'embedding_count': 1, + 'rerank_count': 0, + } + @pytest.mark.usefixtures('mock_circular_import_chain') class TestModelEndpoints: """Tests for /api/v1/provider/models endpoints.""" @pytest.mark.asyncio - async def test_get_llm_models_success(self, quart_test_client): + async def test_get_llm_models_success(self, quart_test_client, fake_provider_app): """GET /api/v1/provider/models/llm returns model list.""" response = await quart_test_client.get( '/api/v1/provider/models/llm', headers={'Authorization': 'Bearer test_token'} @@ -260,8 +294,15 @@ class TestModelEndpoints: assert data['code'] == 0 assert 'data' in data + called = fake_provider_app.llm_model_service.get_llm_models + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == () + assert called.await_args.kwargs == {'include_secret': True} + assert data['data']['models'] == [{'uuid': 'test-model-uuid', 'name': 'gpt-4'}] + @pytest.mark.asyncio - async def test_get_single_llm_model_success(self, quart_test_client): + async def test_get_single_llm_model_success(self, quart_test_client, fake_provider_app): """GET /api/v1/provider/models/llm/{uuid} returns model.""" response = await quart_test_client.get( '/api/v1/provider/models/llm/test-model-uuid', headers={'Authorization': 'Bearer test_token'} @@ -271,8 +312,15 @@ class TestModelEndpoints: data = await response.get_json() assert data['code'] == 0 + called = fake_provider_app.llm_model_service.get_llm_model + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-model-uuid',) + assert called.await_args.kwargs == {'include_secret': True} + assert data['data']['model'] == {'uuid': 'test-model-uuid', 'name': 'gpt-4'} + @pytest.mark.asyncio - async def test_create_llm_model_success(self, quart_test_client): + async def test_create_llm_model_success(self, quart_test_client, fake_provider_app): """POST /api/v1/provider/models/llm creates new model.""" response = await quart_test_client.post( '/api/v1/provider/models/llm', @@ -285,8 +333,15 @@ class TestModelEndpoints: assert data['code'] == 0 assert 'uuid' in data['data'] + called = fake_provider_app.llm_model_service.create_llm_model + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ({'name': 'New Model', 'provider_uuid': 'test-provider-uuid'},) + assert called.await_args.kwargs == {} + assert data['data'] == {'uuid': 'new-model-uuid'} + @pytest.mark.asyncio - async def test_delete_llm_model_success(self, quart_test_client): + async def test_delete_llm_model_success(self, quart_test_client, fake_provider_app): """DELETE /api/v1/provider/models/llm/{uuid} deletes model.""" response = await quart_test_client.delete( '/api/v1/provider/models/llm/test-model-uuid', headers={'Authorization': 'Bearer test_token'} @@ -294,6 +349,13 @@ class TestModelEndpoints: assert response.status_code == 200 + called = fake_provider_app.llm_model_service.delete_llm_model + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ('test-model-uuid',) + assert called.await_args.kwargs == {} + assert (await response.get_json())['code'] == 0 + @pytest.mark.asyncio @pytest.mark.parametrize( ('service_name', 'method_name', 'path'), @@ -337,7 +399,7 @@ class TestEmbeddingModelEndpoints: """Tests for /api/v1/provider/models/embedding endpoints.""" @pytest.mark.asyncio - async def test_get_embedding_models_success(self, quart_test_client): + async def test_get_embedding_models_success(self, quart_test_client, fake_provider_app): """GET /api/v1/provider/models/embedding returns model list.""" response = await quart_test_client.get( '/api/v1/provider/models/embedding', headers={'Authorization': 'Bearer test_token'} @@ -348,8 +410,15 @@ class TestEmbeddingModelEndpoints: assert data['code'] == 0 assert 'models' in data['data'] + called = fake_provider_app.embedding_models_service.get_embedding_models + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == () + assert called.await_args.kwargs == {'include_secret': True} + assert data['data']['models'] == [{'uuid': 'embedding-model', 'name': 'Embedding'}] + @pytest.mark.asyncio - async def test_create_embedding_model_success(self, quart_test_client): + async def test_create_embedding_model_success(self, quart_test_client, fake_provider_app): """POST /api/v1/provider/models/embedding creates new model.""" response = await quart_test_client.post( '/api/v1/provider/models/embedding', @@ -362,13 +431,20 @@ class TestEmbeddingModelEndpoints: assert data['code'] == 0 assert 'uuid' in data['data'] + called = fake_provider_app.embedding_models_service.create_embedding_model + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ({'name': 'New Embedding Model', 'provider_uuid': 'test-provider-uuid'},) + assert called.await_args.kwargs == {} + assert data['data'] == {'uuid': 'new-embedding-uuid'} + @pytest.mark.usefixtures('mock_circular_import_chain') class TestRerankModelEndpoints: """Tests for /api/v1/provider/models/rerank endpoints.""" @pytest.mark.asyncio - async def test_get_rerank_models_success(self, quart_test_client): + async def test_get_rerank_models_success(self, quart_test_client, fake_provider_app): """GET /api/v1/provider/models/rerank returns model list.""" response = await quart_test_client.get( '/api/v1/provider/models/rerank', headers={'Authorization': 'Bearer test_token'} @@ -379,8 +455,15 @@ class TestRerankModelEndpoints: assert data['code'] == 0 assert 'models' in data['data'] + called = fake_provider_app.rerank_models_service.get_rerank_models + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == () + assert called.await_args.kwargs == {'include_secret': True} + assert data['data']['models'] == [{'uuid': 'rerank-model', 'name': 'Rerank'}] + @pytest.mark.asyncio - async def test_create_rerank_model_success(self, quart_test_client): + async def test_create_rerank_model_success(self, quart_test_client, fake_provider_app): """POST /api/v1/provider/models/rerank creates new model.""" response = await quart_test_client.post( '/api/v1/provider/models/rerank', @@ -392,3 +475,10 @@ class TestRerankModelEndpoints: data = await response.get_json() assert data['code'] == 0 assert 'uuid' in data['data'] + + called = fake_provider_app.rerank_models_service.create_rerank_model + assert called.await_count == 1 + assert called.await_args.args[0].workspace_uuid == 'workspace-test' + assert called.await_args.args[1:] == ({'name': 'New Rerank Model', 'provider_uuid': 'test-provider-uuid'},) + assert called.await_args.kwargs == {} + assert data['data'] == {'uuid': 'new-rerank-uuid'}