test(sources): add comprehensive tests for Sprint 2
- Add 28 unit tests for SourceService (TEST-004) - Test all CRUD operations - Test validation logic - Test error handling - Test research functionality - Add 13 integration tests for sources API (TEST-005) - Test POST /sources endpoint - Test GET /sources endpoint with filters - Test DELETE /sources endpoint - Test POST /sources/research endpoint - Fix ValidationError signatures in SourceService - Fix NotebookLMError signatures - Fix status parameter shadowing in sources router Coverage: 28/28 unit tests pass, 13/13 integration tests pass
This commit is contained in:
@@ -144,14 +144,14 @@ async def create_source(notebook_id: str, data: SourceCreate):
|
||||
async def list_sources(
|
||||
notebook_id: str,
|
||||
source_type: str | None = None,
|
||||
status: str | None = None,
|
||||
source_status: str | None = None,
|
||||
):
|
||||
"""List sources for a notebook.
|
||||
|
||||
Args:
|
||||
notebook_id: Notebook UUID.
|
||||
source_type: Optional filter by source type.
|
||||
status: Optional filter by processing status.
|
||||
source_status: Optional filter by processing status.
|
||||
|
||||
Returns:
|
||||
List of sources.
|
||||
@@ -182,7 +182,7 @@ async def list_sources(
|
||||
|
||||
try:
|
||||
service = await get_source_service()
|
||||
sources = await service.list(notebook_uuid, source_type, status)
|
||||
sources = await service.list(notebook_uuid, source_type, source_status)
|
||||
|
||||
return ApiResponse(
|
||||
success=True,
|
||||
@@ -190,7 +190,7 @@ async def list_sources(
|
||||
items=sources,
|
||||
pagination=PaginationMeta(
|
||||
total=len(sources),
|
||||
limit=len(sources),
|
||||
limit=max(len(sources), 1),
|
||||
offset=0,
|
||||
has_more=False,
|
||||
),
|
||||
|
||||
@@ -58,10 +58,7 @@ class SourceService:
|
||||
"""
|
||||
allowed_types = {"url", "file", "youtube", "drive"}
|
||||
if source_type not in allowed_types:
|
||||
raise ValidationError(
|
||||
message=f"Invalid source type. Must be one of: {allowed_types}",
|
||||
code="VALIDATION_ERROR",
|
||||
)
|
||||
raise ValidationError(f"Invalid source type. Must be one of: {allowed_types}")
|
||||
return source_type
|
||||
|
||||
def _validate_url(self, url: str | None, source_type: str) -> str | None:
|
||||
@@ -78,10 +75,7 @@ class SourceService:
|
||||
ValidationError: If URL is required but not provided.
|
||||
"""
|
||||
if source_type in {"url", "youtube"} and not url:
|
||||
raise ValidationError(
|
||||
message=f"URL is required for source type '{source_type}'",
|
||||
code="VALIDATION_ERROR",
|
||||
)
|
||||
raise ValidationError(f"URL is required for source type '{source_type}'")
|
||||
return url
|
||||
|
||||
async def create(self, notebook_id: UUID, data: dict) -> Source:
|
||||
@@ -103,6 +97,12 @@ class SourceService:
|
||||
source_type = data.get("type", "url")
|
||||
self._validate_source_type(source_type)
|
||||
|
||||
# Check for unsupported file type early
|
||||
if source_type == "file":
|
||||
raise ValidationError(
|
||||
"File upload not supported via this method. Use file upload endpoint."
|
||||
)
|
||||
|
||||
url = data.get("url")
|
||||
self._validate_url(url, source_type)
|
||||
|
||||
@@ -119,12 +119,6 @@ class SourceService:
|
||||
result = await notebook.sources.add_youtube(url, title=title)
|
||||
elif source_type == "drive":
|
||||
result = await notebook.sources.add_drive(url, title=title)
|
||||
else:
|
||||
# For file type, this would be handled differently (multipart upload)
|
||||
raise ValidationError(
|
||||
message="File upload not supported via this method. Use file upload endpoint.",
|
||||
code="VALIDATION_ERROR",
|
||||
)
|
||||
|
||||
return Source(
|
||||
id=getattr(result, "id", str(notebook_id)),
|
||||
@@ -136,16 +130,11 @@ class SourceService:
|
||||
created_at=getattr(result, "created_at", datetime.utcnow()),
|
||||
)
|
||||
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if "not found" in error_str:
|
||||
raise NotFoundError("Notebook", str(notebook_id))
|
||||
raise NotebookLMError(
|
||||
message=f"Failed to add source: {e}",
|
||||
code="NOTEBOOKLM_ERROR",
|
||||
)
|
||||
raise NotebookLMError(f"Failed to add source: {e}")
|
||||
|
||||
async def list(
|
||||
self,
|
||||
@@ -201,10 +190,7 @@ class SourceService:
|
||||
error_str = str(e).lower()
|
||||
if "not found" in error_str:
|
||||
raise NotFoundError("Notebook", str(notebook_id))
|
||||
raise NotebookLMError(
|
||||
message=f"Failed to list sources: {e}",
|
||||
code="NOTEBOOKLM_ERROR",
|
||||
)
|
||||
raise NotebookLMError(f"Failed to list sources: {e}")
|
||||
|
||||
async def delete(self, notebook_id: UUID, source_id: str) -> None:
|
||||
"""Delete a source from a notebook.
|
||||
@@ -228,10 +214,7 @@ class SourceService:
|
||||
error_str = str(e).lower()
|
||||
if "not found" in error_str:
|
||||
raise NotFoundError("Source", source_id)
|
||||
raise NotebookLMError(
|
||||
message=f"Failed to delete source: {e}",
|
||||
code="NOTEBOOKLM_ERROR",
|
||||
)
|
||||
raise NotebookLMError(f"Failed to delete source: {e}")
|
||||
|
||||
async def get_fulltext(self, notebook_id: UUID, source_id: str) -> str:
|
||||
"""Get the full text content of a source.
|
||||
@@ -260,10 +243,7 @@ class SourceService:
|
||||
error_str = str(e).lower()
|
||||
if "not found" in error_str:
|
||||
raise NotFoundError("Source", source_id)
|
||||
raise NotebookLMError(
|
||||
message=f"Failed to get source fulltext: {e}",
|
||||
code="NOTEBOOKLM_ERROR",
|
||||
)
|
||||
raise NotebookLMError(f"Failed to get source fulltext: {e}")
|
||||
|
||||
async def research(
|
||||
self,
|
||||
@@ -289,16 +269,10 @@ class SourceService:
|
||||
NotebookLMError: If external API fails.
|
||||
"""
|
||||
if not query or not query.strip():
|
||||
raise ValidationError(
|
||||
message="Query cannot be empty",
|
||||
code="VALIDATION_ERROR",
|
||||
)
|
||||
raise ValidationError("Query cannot be empty")
|
||||
|
||||
if mode not in {"fast", "deep"}:
|
||||
raise ValidationError(
|
||||
message="Mode must be 'fast' or 'deep'",
|
||||
code="VALIDATION_ERROR",
|
||||
)
|
||||
raise ValidationError("Mode must be 'fast' or 'deep'")
|
||||
|
||||
try:
|
||||
client = await self._get_client()
|
||||
@@ -325,7 +299,4 @@ class SourceService:
|
||||
error_str = str(e).lower()
|
||||
if "not found" in error_str:
|
||||
raise NotFoundError("Notebook", str(notebook_id))
|
||||
raise NotebookLMError(
|
||||
message=f"Failed to start research: {e}",
|
||||
code="NOTEBOOKLM_ERROR",
|
||||
)
|
||||
raise NotebookLMError(f"Failed to start research: {e}")
|
||||
|
||||
Reference in New Issue
Block a user