julee 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. julee/__init__.py +3 -0
  2. julee/api/__init__.py +20 -0
  3. julee/api/app.py +180 -0
  4. julee/api/dependencies.py +257 -0
  5. julee/api/requests.py +175 -0
  6. julee/api/responses.py +43 -0
  7. julee/api/routers/__init__.py +43 -0
  8. julee/api/routers/assembly_specifications.py +212 -0
  9. julee/api/routers/documents.py +182 -0
  10. julee/api/routers/knowledge_service_configs.py +79 -0
  11. julee/api/routers/knowledge_service_queries.py +293 -0
  12. julee/api/routers/system.py +137 -0
  13. julee/api/routers/workflows.py +234 -0
  14. julee/api/services/__init__.py +20 -0
  15. julee/api/services/system_initialization.py +214 -0
  16. julee/api/tests/__init__.py +14 -0
  17. julee/api/tests/routers/__init__.py +17 -0
  18. julee/api/tests/routers/test_assembly_specifications.py +749 -0
  19. julee/api/tests/routers/test_documents.py +301 -0
  20. julee/api/tests/routers/test_knowledge_service_configs.py +234 -0
  21. julee/api/tests/routers/test_knowledge_service_queries.py +738 -0
  22. julee/api/tests/routers/test_system.py +179 -0
  23. julee/api/tests/routers/test_workflows.py +393 -0
  24. julee/api/tests/test_app.py +285 -0
  25. julee/api/tests/test_dependencies.py +245 -0
  26. julee/api/tests/test_requests.py +250 -0
  27. julee/domain/__init__.py +22 -0
  28. julee/domain/models/__init__.py +49 -0
  29. julee/domain/models/assembly/__init__.py +17 -0
  30. julee/domain/models/assembly/assembly.py +103 -0
  31. julee/domain/models/assembly/tests/__init__.py +0 -0
  32. julee/domain/models/assembly/tests/factories.py +37 -0
  33. julee/domain/models/assembly/tests/test_assembly.py +430 -0
  34. julee/domain/models/assembly_specification/__init__.py +24 -0
  35. julee/domain/models/assembly_specification/assembly_specification.py +172 -0
  36. julee/domain/models/assembly_specification/knowledge_service_query.py +123 -0
  37. julee/domain/models/assembly_specification/tests/__init__.py +0 -0
  38. julee/domain/models/assembly_specification/tests/factories.py +78 -0
  39. julee/domain/models/assembly_specification/tests/test_assembly_specification.py +490 -0
  40. julee/domain/models/assembly_specification/tests/test_knowledge_service_query.py +310 -0
  41. julee/domain/models/custom_fields/__init__.py +0 -0
  42. julee/domain/models/custom_fields/content_stream.py +68 -0
  43. julee/domain/models/custom_fields/tests/__init__.py +0 -0
  44. julee/domain/models/custom_fields/tests/test_custom_fields.py +53 -0
  45. julee/domain/models/document/__init__.py +17 -0
  46. julee/domain/models/document/document.py +150 -0
  47. julee/domain/models/document/tests/__init__.py +0 -0
  48. julee/domain/models/document/tests/factories.py +76 -0
  49. julee/domain/models/document/tests/test_document.py +297 -0
  50. julee/domain/models/knowledge_service_config/__init__.py +17 -0
  51. julee/domain/models/knowledge_service_config/knowledge_service_config.py +86 -0
  52. julee/domain/models/policy/__init__.py +15 -0
  53. julee/domain/models/policy/document_policy_validation.py +220 -0
  54. julee/domain/models/policy/policy.py +203 -0
  55. julee/domain/models/policy/tests/__init__.py +0 -0
  56. julee/domain/models/policy/tests/factories.py +47 -0
  57. julee/domain/models/policy/tests/test_document_policy_validation.py +420 -0
  58. julee/domain/models/policy/tests/test_policy.py +546 -0
  59. julee/domain/repositories/__init__.py +27 -0
  60. julee/domain/repositories/assembly.py +45 -0
  61. julee/domain/repositories/assembly_specification.py +52 -0
  62. julee/domain/repositories/base.py +146 -0
  63. julee/domain/repositories/document.py +49 -0
  64. julee/domain/repositories/document_policy_validation.py +52 -0
  65. julee/domain/repositories/knowledge_service_config.py +54 -0
  66. julee/domain/repositories/knowledge_service_query.py +44 -0
  67. julee/domain/repositories/policy.py +49 -0
  68. julee/domain/use_cases/__init__.py +17 -0
  69. julee/domain/use_cases/decorators.py +107 -0
  70. julee/domain/use_cases/extract_assemble_data.py +649 -0
  71. julee/domain/use_cases/initialize_system_data.py +842 -0
  72. julee/domain/use_cases/tests/__init__.py +7 -0
  73. julee/domain/use_cases/tests/test_extract_assemble_data.py +548 -0
  74. julee/domain/use_cases/tests/test_initialize_system_data.py +455 -0
  75. julee/domain/use_cases/tests/test_validate_document.py +1228 -0
  76. julee/domain/use_cases/validate_document.py +736 -0
  77. julee/fixtures/assembly_specifications.yaml +70 -0
  78. julee/fixtures/documents.yaml +178 -0
  79. julee/fixtures/knowledge_service_configs.yaml +37 -0
  80. julee/fixtures/knowledge_service_queries.yaml +27 -0
  81. julee/repositories/__init__.py +17 -0
  82. julee/repositories/memory/__init__.py +31 -0
  83. julee/repositories/memory/assembly.py +84 -0
  84. julee/repositories/memory/assembly_specification.py +125 -0
  85. julee/repositories/memory/base.py +227 -0
  86. julee/repositories/memory/document.py +149 -0
  87. julee/repositories/memory/document_policy_validation.py +104 -0
  88. julee/repositories/memory/knowledge_service_config.py +123 -0
  89. julee/repositories/memory/knowledge_service_query.py +120 -0
  90. julee/repositories/memory/policy.py +87 -0
  91. julee/repositories/memory/tests/__init__.py +0 -0
  92. julee/repositories/memory/tests/test_document.py +212 -0
  93. julee/repositories/memory/tests/test_document_policy_validation.py +161 -0
  94. julee/repositories/memory/tests/test_policy.py +443 -0
  95. julee/repositories/minio/__init__.py +31 -0
  96. julee/repositories/minio/assembly.py +103 -0
  97. julee/repositories/minio/assembly_specification.py +170 -0
  98. julee/repositories/minio/client.py +570 -0
  99. julee/repositories/minio/document.py +530 -0
  100. julee/repositories/minio/document_policy_validation.py +120 -0
  101. julee/repositories/minio/knowledge_service_config.py +187 -0
  102. julee/repositories/minio/knowledge_service_query.py +211 -0
  103. julee/repositories/minio/policy.py +106 -0
  104. julee/repositories/minio/tests/__init__.py +0 -0
  105. julee/repositories/minio/tests/fake_client.py +213 -0
  106. julee/repositories/minio/tests/test_assembly.py +374 -0
  107. julee/repositories/minio/tests/test_assembly_specification.py +391 -0
  108. julee/repositories/minio/tests/test_client_protocol.py +57 -0
  109. julee/repositories/minio/tests/test_document.py +591 -0
  110. julee/repositories/minio/tests/test_document_policy_validation.py +192 -0
  111. julee/repositories/minio/tests/test_knowledge_service_config.py +374 -0
  112. julee/repositories/minio/tests/test_knowledge_service_query.py +438 -0
  113. julee/repositories/minio/tests/test_policy.py +559 -0
  114. julee/repositories/temporal/__init__.py +38 -0
  115. julee/repositories/temporal/activities.py +114 -0
  116. julee/repositories/temporal/activity_names.py +34 -0
  117. julee/repositories/temporal/proxies.py +159 -0
  118. julee/services/__init__.py +18 -0
  119. julee/services/knowledge_service/__init__.py +48 -0
  120. julee/services/knowledge_service/anthropic/__init__.py +12 -0
  121. julee/services/knowledge_service/anthropic/knowledge_service.py +331 -0
  122. julee/services/knowledge_service/anthropic/tests/test_knowledge_service.py +318 -0
  123. julee/services/knowledge_service/factory.py +138 -0
  124. julee/services/knowledge_service/knowledge_service.py +160 -0
  125. julee/services/knowledge_service/memory/__init__.py +13 -0
  126. julee/services/knowledge_service/memory/knowledge_service.py +278 -0
  127. julee/services/knowledge_service/memory/test_knowledge_service.py +345 -0
  128. julee/services/knowledge_service/test_factory.py +112 -0
  129. julee/services/temporal/__init__.py +38 -0
  130. julee/services/temporal/activities.py +86 -0
  131. julee/services/temporal/activity_names.py +22 -0
  132. julee/services/temporal/proxies.py +41 -0
  133. julee/util/__init__.py +0 -0
  134. julee/util/domain.py +119 -0
  135. julee/util/repos/__init__.py +0 -0
  136. julee/util/repos/minio/__init__.py +0 -0
  137. julee/util/repos/minio/file_storage.py +213 -0
  138. julee/util/repos/temporal/__init__.py +11 -0
  139. julee/util/repos/temporal/client_proxies/file_storage.py +68 -0
  140. julee/util/repos/temporal/data_converter.py +123 -0
  141. julee/util/repos/temporal/minio_file_storage.py +12 -0
  142. julee/util/repos/temporal/proxies/__init__.py +0 -0
  143. julee/util/repos/temporal/proxies/file_storage.py +58 -0
  144. julee/util/repositories.py +55 -0
  145. julee/util/temporal/__init__.py +22 -0
  146. julee/util/temporal/activities.py +123 -0
  147. julee/util/temporal/decorators.py +473 -0
  148. julee/util/tests/__init__.py +1 -0
  149. julee/util/tests/test_decorators.py +770 -0
  150. julee/util/validation/__init__.py +29 -0
  151. julee/util/validation/repository.py +100 -0
  152. julee/util/validation/type_guards.py +369 -0
  153. julee/worker.py +211 -0
  154. julee/workflows/__init__.py +26 -0
  155. julee/workflows/extract_assemble.py +215 -0
  156. julee/workflows/validate_document.py +228 -0
  157. julee-0.1.0.dist-info/METADATA +195 -0
  158. julee-0.1.0.dist-info/RECORD +161 -0
  159. julee-0.1.0.dist-info/WHEEL +5 -0
  160. julee-0.1.0.dist-info/licenses/LICENSE +674 -0
  161. julee-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,301 @@
1
+ """
2
+ Tests for documents API router.
3
+
4
+ This module provides unit tests for the documents API endpoints,
5
+ focusing on the core functionality of listing documents with pagination.
6
+ """
7
+
8
+ import pytest
9
+ from datetime import datetime, timezone
10
+ from typing import Generator
11
+ from fastapi.testclient import TestClient
12
+ from fastapi import FastAPI
13
+ from fastapi_pagination import add_pagination
14
+
15
+ from julee.api.routers.documents import router
16
+ from julee.api.dependencies import get_document_repository
17
+ from julee.domain.models.document import Document, DocumentStatus
18
+ from julee.repositories.memory import MemoryDocumentRepository
19
+
20
+
21
+ @pytest.fixture
22
+ def memory_repo() -> MemoryDocumentRepository:
23
+ """Create a memory document repository for testing."""
24
+ return MemoryDocumentRepository()
25
+
26
+
27
+ @pytest.fixture
28
+ def app(memory_repo: MemoryDocumentRepository) -> FastAPI:
29
+ """Create FastAPI app with documents router for testing."""
30
+ app = FastAPI()
31
+
32
+ # Override the dependency with our memory repository
33
+ app.dependency_overrides[get_document_repository] = lambda: memory_repo
34
+
35
+ # Add pagination support (required for the paginate function)
36
+ add_pagination(app)
37
+
38
+ app.include_router(router, prefix="/documents")
39
+ return app
40
+
41
+
42
+ @pytest.fixture
43
+ def client(app: FastAPI) -> Generator[TestClient, None, None]:
44
+ """Create test client."""
45
+ with TestClient(app) as test_client:
46
+ yield test_client
47
+
48
+
49
+ @pytest.fixture
50
+ def sample_documents() -> list[Document]:
51
+ """Create sample documents for testing."""
52
+ return [
53
+ Document(
54
+ document_id="doc-1",
55
+ original_filename="test-document-1.txt",
56
+ content_type="text/plain",
57
+ size_bytes=1024,
58
+ content_multihash="QmTest1",
59
+ status=DocumentStatus.CAPTURED,
60
+ created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
61
+ updated_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
62
+ additional_metadata={"type": "test"},
63
+ content_string="test content",
64
+ ),
65
+ Document(
66
+ document_id="doc-2",
67
+ original_filename="test-document-2.pdf",
68
+ content_type="application/pdf",
69
+ size_bytes=2048,
70
+ content_multihash="QmTest2",
71
+ status=DocumentStatus.REGISTERED,
72
+ created_at=datetime(2024, 1, 2, 12, 0, 0, tzinfo=timezone.utc),
73
+ updated_at=datetime(2024, 1, 2, 12, 0, 0, tzinfo=timezone.utc),
74
+ additional_metadata={"type": "report"},
75
+ content_string="pdf content",
76
+ ),
77
+ ]
78
+
79
+
80
+ class TestListDocuments:
81
+ """Test cases for the list documents endpoint."""
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_list_documents_success(
85
+ self,
86
+ client: TestClient,
87
+ memory_repo: MemoryDocumentRepository,
88
+ sample_documents: list[Document],
89
+ ) -> None:
90
+ """Test successful document listing."""
91
+ # Setup - add documents to repository
92
+ for doc in sample_documents:
93
+ await memory_repo.save(doc)
94
+
95
+ # Make request
96
+ response = client.get("/documents/")
97
+
98
+ # Assertions
99
+ assert response.status_code == 200
100
+ data = response.json()
101
+
102
+ assert data["total"] == 2
103
+ assert data["page"] == 1
104
+ assert data["size"] == 50 # Default fastapi-pagination size
105
+ assert data["pages"] == 1
106
+ assert len(data["items"]) == 2
107
+
108
+ # Check first document (documents may not be in insertion order)
109
+ doc_ids = [item["document_id"] for item in data["items"]]
110
+ assert "doc-1" in doc_ids
111
+ assert "doc-2" in doc_ids
112
+
113
+ # Find doc-1 and verify its details
114
+ doc1 = next(item for item in data["items"] if item["document_id"] == "doc-1")
115
+ assert doc1["original_filename"] == "test-document-1.txt"
116
+ assert doc1["content_type"] == "text/plain"
117
+ assert doc1["size_bytes"] == 12 # Length of "test content"
118
+ assert doc1["status"] == "captured"
119
+ assert doc1["additional_metadata"] == {"type": "test"}
120
+
121
+ @pytest.mark.asyncio
122
+ async def test_list_documents_with_pagination(
123
+ self,
124
+ client: TestClient,
125
+ memory_repo: MemoryDocumentRepository,
126
+ sample_documents: list[Document],
127
+ ) -> None:
128
+ """Test document listing with custom pagination."""
129
+ # Setup - add documents to repository
130
+ for doc in sample_documents:
131
+ await memory_repo.save(doc)
132
+
133
+ # Make request with pagination
134
+ response = client.get("/documents/?page=1&size=1")
135
+
136
+ # Assertions
137
+ assert response.status_code == 200
138
+ data = response.json()
139
+
140
+ assert data["total"] == 2
141
+ assert data["page"] == 1
142
+ assert data["size"] == 1
143
+ assert data["pages"] == 2
144
+ assert len(data["items"]) == 1
145
+
146
+ def test_list_documents_empty_result(
147
+ self, client: TestClient, memory_repo: MemoryDocumentRepository
148
+ ) -> None:
149
+ """Test document listing when no documents exist."""
150
+ # No setup needed - memory repo starts empty
151
+
152
+ # Make request
153
+ response = client.get("/documents/")
154
+
155
+ # Assertions
156
+ assert response.status_code == 200
157
+ data = response.json()
158
+
159
+ assert data["total"] == 0
160
+ assert data["page"] == 1
161
+ assert data["size"] == 50 # Default fastapi-pagination size
162
+ assert data["pages"] == 0
163
+ assert len(data["items"]) == 0
164
+
165
+ def test_list_documents_invalid_page(self, client: TestClient) -> None:
166
+ """Test document listing with invalid page parameter."""
167
+ response = client.get("/documents/?page=0")
168
+ assert response.status_code == 422 # Validation error
169
+
170
+ def test_list_documents_invalid_size(self, client: TestClient) -> None:
171
+ """Test document listing with invalid size parameter."""
172
+ response = client.get("/documents/?size=101")
173
+ assert response.status_code == 422 # Validation error
174
+
175
+
176
+ class TestGetDocument:
177
+ """Test cases for the get document metadata endpoint."""
178
+
179
+ @pytest.mark.asyncio
180
+ async def test_get_document_metadata_success(
181
+ self,
182
+ client: TestClient,
183
+ memory_repo: MemoryDocumentRepository,
184
+ sample_documents: list[Document],
185
+ ) -> None:
186
+ """Test successful document metadata retrieval."""
187
+ # Setup - add document to repository
188
+ doc = sample_documents[0]
189
+ await memory_repo.save(doc)
190
+
191
+ # Make request
192
+ response = client.get(f"/documents/{doc.document_id}")
193
+
194
+ # Assertions
195
+ assert response.status_code == 200
196
+ data = response.json()
197
+
198
+ assert data["document_id"] == doc.document_id
199
+ assert data["original_filename"] == doc.original_filename
200
+ assert data["content_type"] == doc.content_type
201
+ assert data["status"] == doc.status.value
202
+ assert data["additional_metadata"] == doc.additional_metadata
203
+
204
+ # Content should NOT be included in metadata endpoint
205
+ assert data["content_string"] is None
206
+ # Content field is excluded from JSON response
207
+ assert "content" not in data
208
+
209
+ @pytest.mark.asyncio
210
+ async def test_get_document_metadata_not_found(
211
+ self, client: TestClient, memory_repo: MemoryDocumentRepository
212
+ ) -> None:
213
+ """Test document metadata retrieval when document doesn't exist."""
214
+ response = client.get("/documents/nonexistent-id")
215
+
216
+ assert response.status_code == 404
217
+ data = response.json()
218
+ assert "not found" in data["detail"].lower()
219
+
220
+ def test_get_document_metadata_invalid_id_format(self, client: TestClient) -> None:
221
+ """Test document metadata retrieval with invalid ID format."""
222
+ # Test with empty ID (should be handled by FastAPI path validation)
223
+ response = client.get("/documents/")
224
+ # This should hit the list endpoint instead
225
+ assert response.status_code == 200
226
+
227
+ # Test with very long ID
228
+ very_long_id = "x" * 1000
229
+ response = client.get(f"/documents/{very_long_id}")
230
+ assert response.status_code == 404 # Not found, but valid request
231
+
232
+
233
+ class TestGetDocumentContent:
234
+ """Test cases for the get document content endpoint."""
235
+
236
+ @pytest.mark.asyncio
237
+ async def test_get_document_content_success(
238
+ self,
239
+ client: TestClient,
240
+ memory_repo: MemoryDocumentRepository,
241
+ sample_documents: list[Document],
242
+ ) -> None:
243
+ """Test successful document content retrieval."""
244
+ # Setup - add document to repository
245
+ doc = sample_documents[0]
246
+ await memory_repo.save(doc)
247
+
248
+ # Make request
249
+ response = client.get(f"/documents/{doc.document_id}/content")
250
+
251
+ # Assertions
252
+ assert response.status_code == 200
253
+ assert response.content.decode("utf-8") == "test content"
254
+ assert response.headers["content-type"].startswith(doc.content_type)
255
+ assert doc.original_filename in response.headers["content-disposition"]
256
+
257
+ @pytest.mark.asyncio
258
+ async def test_get_document_content_not_found(
259
+ self, client: TestClient, memory_repo: MemoryDocumentRepository
260
+ ) -> None:
261
+ """Test content retrieval when document doesn't exist."""
262
+ response = client.get("/documents/nonexistent-id/content")
263
+
264
+ assert response.status_code == 404
265
+ data = response.json()
266
+ assert "not found" in data["detail"].lower()
267
+
268
+ @pytest.mark.asyncio
269
+ async def test_get_document_content_no_content(
270
+ self,
271
+ client: TestClient,
272
+ memory_repo: MemoryDocumentRepository,
273
+ ) -> None:
274
+ """Test content retrieval when document has no content."""
275
+ # Create document with content_string first to pass validation
276
+ doc = Document(
277
+ document_id="doc-no-content",
278
+ original_filename="empty.txt",
279
+ content_type="text/plain",
280
+ size_bytes=1,
281
+ content_multihash="empty_hash",
282
+ status=DocumentStatus.CAPTURED,
283
+ additional_metadata={"type": "empty"},
284
+ content_string="temp",
285
+ )
286
+
287
+ # Save document normally, then manually remove content from storage
288
+ await memory_repo.save(doc)
289
+ stored_doc = memory_repo.storage_dict[doc.document_id]
290
+ # Remove content from the stored document
291
+ memory_repo.storage_dict[doc.document_id] = stored_doc.model_copy(
292
+ update={"content": None, "content_string": None}
293
+ )
294
+
295
+ # Make request
296
+ response = client.get(f"/documents/{doc.document_id}/content")
297
+
298
+ # Assertions
299
+ assert response.status_code == 422
300
+ data = response.json()
301
+ assert "has no content" in data["detail"].lower()
@@ -0,0 +1,234 @@
1
+ """
2
+ Tests for knowledge service configurations API endpoints.
3
+
4
+ This module tests the API endpoints for knowledge service configurations,
5
+ ensuring they follow consistent patterns with proper error handling,
6
+ pagination, and response formats.
7
+ """
8
+
9
+ import pytest
10
+ from unittest.mock import AsyncMock
11
+ from fastapi.testclient import TestClient
12
+ from typing import Generator
13
+
14
+ from julee.api.app import app
15
+ from julee.domain.models.knowledge_service_config import (
16
+ KnowledgeServiceConfig,
17
+ ServiceApi,
18
+ )
19
+ from julee.api.dependencies import (
20
+ get_knowledge_service_config_repository,
21
+ )
22
+ from datetime import datetime, timezone
23
+
24
+
25
+ @pytest.fixture
26
+ def mock_repository() -> AsyncMock:
27
+ """Create mock knowledge service config repository."""
28
+ return AsyncMock()
29
+
30
+
31
+ @pytest.fixture
32
+ def client(mock_repository: AsyncMock) -> Generator[TestClient, None, None]:
33
+ """Create test client with mocked dependencies."""
34
+ app.dependency_overrides[get_knowledge_service_config_repository] = (
35
+ lambda: mock_repository
36
+ )
37
+
38
+ with TestClient(app) as test_client:
39
+ yield test_client
40
+
41
+ # Clean up
42
+ app.dependency_overrides.clear()
43
+
44
+
45
+ @pytest.fixture
46
+ def sample_configs() -> list[KnowledgeServiceConfig]:
47
+ """Sample knowledge service configurations for testing."""
48
+ return [
49
+ KnowledgeServiceConfig(
50
+ knowledge_service_id="anthropic-claude",
51
+ name="Anthropic Claude",
52
+ description="Claude 3 for general text analysis and extraction",
53
+ service_api=ServiceApi.ANTHROPIC,
54
+ created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
55
+ updated_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
56
+ ),
57
+ KnowledgeServiceConfig(
58
+ knowledge_service_id="openai-gpt4",
59
+ name="OpenAI GPT-4",
60
+ description="GPT-4 for comprehensive text understanding",
61
+ service_api=ServiceApi.ANTHROPIC, # Only enum value available
62
+ created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
63
+ updated_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
64
+ ),
65
+ KnowledgeServiceConfig(
66
+ knowledge_service_id="memory-service",
67
+ name="Memory Service",
68
+ description="In-memory service for testing and development",
69
+ service_api=ServiceApi.ANTHROPIC, # Only enum value available
70
+ created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
71
+ updated_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
72
+ ),
73
+ ]
74
+
75
+
76
+ class TestGetKnowledgeServiceConfigs:
77
+ """Test GET /knowledge_service_configs/ endpoint."""
78
+
79
+ def test_get_configs_success(
80
+ self,
81
+ client: TestClient,
82
+ mock_repository: AsyncMock,
83
+ sample_configs: list[KnowledgeServiceConfig],
84
+ ) -> None:
85
+ """Test successful retrieval of knowledge service configurations."""
86
+ # Setup mock
87
+ mock_repository.list_all.return_value = sample_configs
88
+
89
+ # Make request
90
+ response = client.get("/knowledge_service_configs/")
91
+
92
+ # Assert response
93
+ assert response.status_code == 200
94
+ data = response.json()
95
+
96
+ # Check pagination structure
97
+ assert "items" in data
98
+ assert "total" in data
99
+ assert "page" in data
100
+ assert "size" in data
101
+
102
+ # Check data content
103
+ assert len(data["items"]) == 3
104
+ assert data["total"] == 3
105
+
106
+ # Verify first config details
107
+ first_config = data["items"][0]
108
+ assert first_config["knowledge_service_id"] == "anthropic-claude"
109
+ assert first_config["name"] == "Anthropic Claude"
110
+ assert (
111
+ first_config["description"]
112
+ == "Claude 3 for general text analysis and extraction"
113
+ )
114
+ assert first_config["service_api"] == "anthropic"
115
+
116
+ # Verify repository was called
117
+ mock_repository.list_all.assert_called_once()
118
+
119
+ def test_get_configs_empty_list(
120
+ self, client: TestClient, mock_repository: AsyncMock
121
+ ) -> None:
122
+ """Test successful retrieval when no configurations exist."""
123
+ # Setup mock
124
+ mock_repository.list_all.return_value = []
125
+
126
+ # Make request
127
+ response = client.get("/knowledge_service_configs/")
128
+
129
+ # Assert response
130
+ assert response.status_code == 200
131
+ data = response.json()
132
+
133
+ assert data["items"] == []
134
+ assert data["total"] == 0
135
+
136
+ # Verify repository was called
137
+ mock_repository.list_all.assert_called_once()
138
+
139
+ def test_get_configs_single_config(
140
+ self,
141
+ client: TestClient,
142
+ mock_repository: AsyncMock,
143
+ sample_configs: list[KnowledgeServiceConfig],
144
+ ) -> None:
145
+ """Test successful retrieval with a single configuration."""
146
+ # Setup mock with single config
147
+ single_config = [sample_configs[0]]
148
+ mock_repository.list_all.return_value = single_config
149
+
150
+ # Make request
151
+ response = client.get("/knowledge_service_configs/")
152
+
153
+ # Assert response
154
+ assert response.status_code == 200
155
+ data = response.json()
156
+
157
+ assert len(data["items"]) == 1
158
+ assert data["total"] == 1
159
+ assert data["items"][0]["knowledge_service_id"] == "anthropic-claude"
160
+
161
+ # Verify repository was called
162
+ mock_repository.list_all.assert_called_once()
163
+
164
+ def test_get_configs_repository_error(
165
+ self, client: TestClient, mock_repository: AsyncMock
166
+ ) -> None:
167
+ """Test handling of repository errors."""
168
+ # Setup mock to raise exception
169
+ mock_repository.list_all.side_effect = Exception("Database connection failed")
170
+
171
+ # Make request
172
+ response = client.get("/knowledge_service_configs/")
173
+
174
+ # Assert error response
175
+ assert response.status_code == 500
176
+ data = response.json()
177
+ assert "detail" in data
178
+ assert "internal error" in data["detail"].lower()
179
+
180
+ # Verify repository was called
181
+ mock_repository.list_all.assert_called_once()
182
+
183
+ def test_get_configs_response_structure(
184
+ self,
185
+ client: TestClient,
186
+ mock_repository: AsyncMock,
187
+ sample_configs: list[KnowledgeServiceConfig],
188
+ ) -> None:
189
+ """Test that response follows expected pagination structure."""
190
+ # Setup mock
191
+ mock_repository.list_all.return_value = sample_configs
192
+
193
+ # Make request
194
+ response = client.get("/knowledge_service_configs/")
195
+
196
+ # Assert response structure
197
+ assert response.status_code == 200
198
+ data = response.json()
199
+
200
+ # Check required pagination fields
201
+ required_fields = ["items", "total", "page", "size", "pages"]
202
+ for field in required_fields:
203
+ assert field in data, f"Missing required field: {field}"
204
+
205
+ # Check item structure
206
+ if data["items"]:
207
+ item = data["items"][0]
208
+ required_item_fields = [
209
+ "knowledge_service_id",
210
+ "name",
211
+ "description",
212
+ "service_api",
213
+ "created_at",
214
+ "updated_at",
215
+ ]
216
+ for field in required_item_fields:
217
+ assert field in item, f"Missing required item field: {field}"
218
+
219
+ def test_get_configs_content_type(
220
+ self,
221
+ client: TestClient,
222
+ mock_repository: AsyncMock,
223
+ sample_configs: list[KnowledgeServiceConfig],
224
+ ) -> None:
225
+ """Test that response has correct content type."""
226
+ # Setup mock
227
+ mock_repository.list_all.return_value = sample_configs
228
+
229
+ # Make request
230
+ response = client.get("/knowledge_service_configs/")
231
+
232
+ # Assert content type
233
+ assert response.status_code == 200
234
+ assert "application/json" in response.headers["content-type"]