mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 20:15:30 +00:00
More tests
This commit is contained in:
1 parent
d5c0322e2a
commit
a3b08a5b44
37 files changed
+21904
-6
No files matched your search
@@ -361,3 +361,218 @@ class TestCheckUsageStringBooleans:
|
||||
result = resource.check_usage({"user_api_key": "str_bool_key"})
|
||||
# Should not exceed limits, so returns None
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompleteStreamCompressionMetadata:
|
||||
"""Cover lines 307-319 (compression metadata persistence in complete_stream)."""
|
||||
|
||||
def test_compression_metadata_persisted(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter(
|
||||
[
|
||||
{"answer": "compressed answer"},
|
||||
]
|
||||
)
|
||||
mock_agent.compression_metadata = {"ratio": 2.5}
|
||||
mock_agent.compression_saved = False
|
||||
mock_agent.tool_calls = []
|
||||
|
||||
resource.conversation_service = MagicMock()
|
||||
resource.conversation_service.save_conversation.return_value = "conv123"
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=True,
|
||||
model_id="gpt-4",
|
||||
)
|
||||
)
|
||||
|
||||
# Verify compression metadata was persisted
|
||||
resource.conversation_service.update_compression_metadata.assert_called_once_with(
|
||||
"conv123", {"ratio": 2.5}
|
||||
)
|
||||
resource.conversation_service.append_compression_message.assert_called_once()
|
||||
assert mock_agent.compression_saved is True
|
||||
end_chunks = [s for s in stream if '"type": "end"' in s]
|
||||
assert len(end_chunks) == 1
|
||||
|
||||
def test_compression_metadata_error_handled(self, mock_mongo_db, flask_app):
|
||||
"""Cover lines 318-322: compression metadata persistence error."""
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.gen.return_value = iter([{"answer": "answer"}])
|
||||
mock_agent.compression_metadata = {"ratio": 2.5}
|
||||
mock_agent.compression_saved = False
|
||||
mock_agent.tool_calls = []
|
||||
|
||||
resource.conversation_service = MagicMock()
|
||||
resource.conversation_service.save_conversation.return_value = "conv123"
|
||||
resource.conversation_service.update_compression_metadata.side_effect = (
|
||||
Exception("db error")
|
||||
)
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=True,
|
||||
model_id="gpt-4",
|
||||
)
|
||||
)
|
||||
|
||||
# Stream should still complete despite compression error
|
||||
end_chunks = [s for s in stream if '"type": "end"' in s]
|
||||
assert len(end_chunks) == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompleteStreamLogTruncation:
|
||||
"""Cover line 354: log data truncation for long values."""
|
||||
|
||||
def test_long_response_truncated_in_log(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
long_answer = "x" * 20000
|
||||
mock_agent.gen.return_value = iter([{"answer": long_answer}])
|
||||
mock_agent.tool_calls = []
|
||||
|
||||
stream = list(
|
||||
resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id=None,
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=False,
|
||||
)
|
||||
)
|
||||
|
||||
end_chunks = [s for s in stream if '"type": "end"' in s]
|
||||
assert len(end_chunks) == 1
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompleteStreamGeneratorExit:
|
||||
"""Cover lines 360-416 (GeneratorExit handling in complete_stream)."""
|
||||
|
||||
def test_generator_exit_saves_partial_response(self, mock_mongo_db, flask_app):
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
|
||||
def gen_with_answers():
|
||||
yield {"answer": "partial"}
|
||||
yield {"answer": " answer"}
|
||||
# Simulating a long stream that gets interrupted
|
||||
yield {"answer": " more"}
|
||||
|
||||
mock_agent.gen.return_value = gen_with_answers()
|
||||
mock_agent.compression_metadata = None
|
||||
mock_agent.compression_saved = False
|
||||
mock_agent.tool_calls = []
|
||||
|
||||
resource.conversation_service = MagicMock()
|
||||
resource.conversation_service.save_conversation.return_value = "conv1"
|
||||
|
||||
gen = resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id="conv1",
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=True,
|
||||
model_id="gpt-4",
|
||||
)
|
||||
|
||||
# Read first chunk and then close (simulating client disconnect)
|
||||
chunk = next(gen)
|
||||
assert "partial" in chunk
|
||||
gen.close() # This triggers GeneratorExit
|
||||
|
||||
def test_generator_exit_with_compression_metadata(self, mock_mongo_db, flask_app):
|
||||
"""Cover lines 393-411: GeneratorExit with compression metadata."""
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
|
||||
def gen_answers():
|
||||
yield {"answer": "partial answer"}
|
||||
|
||||
mock_agent.gen.return_value = gen_answers()
|
||||
mock_agent.compression_metadata = {"ratio": 3.0}
|
||||
mock_agent.compression_saved = False
|
||||
mock_agent.tool_calls = []
|
||||
|
||||
resource.conversation_service = MagicMock()
|
||||
resource.conversation_service.save_conversation.return_value = "conv1"
|
||||
|
||||
gen = resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id="conv1",
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=True,
|
||||
model_id="gpt-4",
|
||||
isNoneDoc=True,
|
||||
)
|
||||
|
||||
next(gen)
|
||||
gen.close()
|
||||
|
||||
def test_generator_exit_save_error_handled(self, mock_mongo_db, flask_app):
|
||||
"""Cover lines 412-415: exception during partial save."""
|
||||
from application.api.answer.routes.base import BaseAnswerResource
|
||||
|
||||
with flask_app.app_context():
|
||||
resource = BaseAnswerResource()
|
||||
mock_agent = MagicMock()
|
||||
|
||||
def gen_answers():
|
||||
yield {"answer": "partial"}
|
||||
|
||||
mock_agent.gen.return_value = gen_answers()
|
||||
mock_agent.compression_metadata = None
|
||||
mock_agent.compression_saved = False
|
||||
mock_agent.tool_calls = []
|
||||
|
||||
resource.conversation_service = MagicMock()
|
||||
resource.conversation_service.save_conversation.side_effect = Exception(
|
||||
"save error"
|
||||
)
|
||||
|
||||
gen = resource.complete_stream(
|
||||
question="Q",
|
||||
agent=mock_agent,
|
||||
conversation_id="conv1",
|
||||
user_api_key=None,
|
||||
decoded_token={"sub": "u"},
|
||||
should_save_conversation=True,
|
||||
model_id="gpt-4",
|
||||
)
|
||||
|
||||
next(gen)
|
||||
gen.close() # Should not crash even with save error
|
||||
@@ -9,7 +9,7 @@ Additional coverage beyond tests/api/answer/services/test_conversation_service.p
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import MagicMock, Mock
|
||||
|
||||
import pytest
|
||||
from bson import ObjectId
|
||||
@@ -416,3 +416,63 @@ class TestGetCompressionMetadata:
|
||||
service = ConversationService()
|
||||
result = service.get_compression_metadata("invalid-id")
|
||||
assert result is None
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Coverage gap tests (lines 233-237, 258, 261)
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConversationServiceGaps:
|
||||
|
||||
def test_update_compression_metadata_exception_raises(self, mock_mongo_db):
|
||||
"""Cover lines 233-237: exception during update raises."""
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
|
||||
service = ConversationService()
|
||||
service.conversations_collection = MagicMock()
|
||||
service.conversations_collection.update_one.side_effect = Exception("db error")
|
||||
|
||||
with pytest.raises(Exception, match="db error"):
|
||||
service.update_compression_metadata(
|
||||
str(ObjectId()),
|
||||
{
|
||||
"compressed_summary": "summary",
|
||||
"query_index": 5,
|
||||
"compressed_token_count": 100,
|
||||
"original_token_count": 1000,
|
||||
},
|
||||
)
|
||||
|
||||
def test_append_compression_message_with_summary(self, mock_mongo_db):
|
||||
"""Cover lines 258, 261: appends compression message to conversation."""
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
|
||||
service = ConversationService()
|
||||
service.conversations_collection = MagicMock()
|
||||
|
||||
conv_id = str(ObjectId())
|
||||
metadata = {
|
||||
"compressed_summary": "This is a summary of the conversation.",
|
||||
"timestamp": "2024-01-01T00:00:00",
|
||||
"model_used": "gpt-4",
|
||||
}
|
||||
service.append_compression_message(conv_id, metadata)
|
||||
service.conversations_collection.update_one.assert_called_once()
|
||||
|
||||
def test_append_compression_message_empty_summary_skips(self, mock_mongo_db):
|
||||
"""Cover: empty summary does not insert."""
|
||||
from application.api.answer.services.conversation_service import (
|
||||
ConversationService,
|
||||
)
|
||||
|
||||
service = ConversationService()
|
||||
service.conversations_collection = MagicMock()
|
||||
|
||||
service.append_compression_message(str(ObjectId()), {"compressed_summary": ""})
|
||||
service.conversations_collection.update_one.assert_not_called()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,6 @@
|
||||
"""Tests for application/api/connector/routes.py"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@@ -331,3 +332,490 @@ class TestBuildCallbackRedirect:
|
||||
url = build_callback_redirect({"status": "success", "message": "OK"})
|
||||
assert url.startswith("/api/connectors/callback-status?")
|
||||
assert "status=success" in url
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConnectorsCallback:
|
||||
"""Tests for the ConnectorsCallback OAuth callback route."""
|
||||
|
||||
def _encode_state(self, state_dict):
|
||||
return base64.urlsafe_b64encode(json.dumps(state_dict).encode()).decode()
|
||||
|
||||
def _patch_connector_creator(self):
|
||||
"""Patch ConnectorCreator at both module-level and local-import locations."""
|
||||
return patch(
|
||||
"application.parser.connectors.connector_creator.ConnectorCreator",
|
||||
)
|
||||
|
||||
def test_callback_invalid_provider_redirects_error(self, client, mock_sessions):
|
||||
state = self._encode_state({"provider": "dropbox", "object_id": "abc123"})
|
||||
with self._patch_connector_creator() as MockCC:
|
||||
MockCC.is_supported.return_value = False
|
||||
resp = client.get(
|
||||
f"/api/connectors/callback?code=auth_code&state={state}"
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert "error" in resp.headers.get("Location", "")
|
||||
|
||||
def test_callback_access_denied_redirects_cancelled(self, client, mock_sessions):
|
||||
state = self._encode_state(
|
||||
{"provider": "google_drive", "object_id": "abc123"}
|
||||
)
|
||||
with self._patch_connector_creator() as MockCC:
|
||||
MockCC.is_supported.return_value = True
|
||||
resp = client.get(
|
||||
f"/api/connectors/callback?error=access_denied&state={state}"
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert "cancelled" in resp.headers.get("Location", "")
|
||||
|
||||
def test_callback_other_error_redirects_error(self, client, mock_sessions):
|
||||
state = self._encode_state(
|
||||
{"provider": "google_drive", "object_id": "abc123"}
|
||||
)
|
||||
with self._patch_connector_creator() as MockCC:
|
||||
MockCC.is_supported.return_value = True
|
||||
resp = client.get(
|
||||
f"/api/connectors/callback?error=server_error&state={state}"
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert "error" in resp.headers.get("Location", "")
|
||||
|
||||
def test_callback_missing_code_redirects_error(self, client, mock_sessions):
|
||||
state = self._encode_state(
|
||||
{"provider": "google_drive", "object_id": "abc123"}
|
||||
)
|
||||
with self._patch_connector_creator() as MockCC:
|
||||
MockCC.is_supported.return_value = True
|
||||
resp = client.get(f"/api/connectors/callback?state={state}")
|
||||
assert resp.status_code == 302
|
||||
assert "error" in resp.headers.get("Location", "")
|
||||
|
||||
def test_callback_success_google_drive(self, client, mock_sessions):
|
||||
oid = mock_sessions["sessions"].insert_one(
|
||||
{
|
||||
"provider": "google_drive",
|
||||
"user": "test_user",
|
||||
"status": "pending",
|
||||
}
|
||||
).inserted_id
|
||||
state = self._encode_state(
|
||||
{"provider": "google_drive", "object_id": str(oid)}
|
||||
)
|
||||
with self._patch_connector_creator() as MockCC:
|
||||
MockCC.is_supported.return_value = True
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.exchange_code_for_tokens.return_value = {
|
||||
"access_token": "at",
|
||||
"refresh_token": "rt",
|
||||
}
|
||||
mock_creds = MagicMock()
|
||||
mock_auth.create_credentials_from_token_info.return_value = mock_creds
|
||||
mock_service = MagicMock()
|
||||
mock_service.about.return_value.get.return_value.execute.return_value = {
|
||||
"user": {"emailAddress": "user@example.com"}
|
||||
}
|
||||
mock_auth.build_drive_service.return_value = mock_service
|
||||
mock_auth.sanitize_token_info.return_value = {
|
||||
"access_token": "at",
|
||||
"refresh_token": "rt",
|
||||
}
|
||||
MockCC.create_auth.return_value = mock_auth
|
||||
|
||||
resp = client.get(
|
||||
f"/api/connectors/callback?code=auth_code&state={state}"
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert "success" in resp.headers.get("Location", "")
|
||||
|
||||
def test_callback_success_non_google_provider(self, client, mock_sessions):
|
||||
oid = mock_sessions["sessions"].insert_one(
|
||||
{
|
||||
"provider": "other_provider",
|
||||
"user": "test_user",
|
||||
"status": "pending",
|
||||
}
|
||||
).inserted_id
|
||||
state = self._encode_state(
|
||||
{"provider": "other_provider", "object_id": str(oid)}
|
||||
)
|
||||
with self._patch_connector_creator() as MockCC:
|
||||
MockCC.is_supported.return_value = True
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.exchange_code_for_tokens.return_value = {
|
||||
"access_token": "at",
|
||||
"user_info": {"email": "other@example.com"},
|
||||
}
|
||||
mock_auth.sanitize_token_info.return_value = {"access_token": "at"}
|
||||
MockCC.create_auth.return_value = mock_auth
|
||||
|
||||
resp = client.get(
|
||||
f"/api/connectors/callback?code=auth_code&state={state}"
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert "success" in resp.headers.get("Location", "")
|
||||
|
||||
def test_callback_exchange_tokens_fails(self, client, mock_sessions):
|
||||
oid = mock_sessions["sessions"].insert_one(
|
||||
{
|
||||
"provider": "google_drive",
|
||||
"user": "test_user",
|
||||
"status": "pending",
|
||||
}
|
||||
).inserted_id
|
||||
state = self._encode_state(
|
||||
{"provider": "google_drive", "object_id": str(oid)}
|
||||
)
|
||||
with self._patch_connector_creator() as MockCC:
|
||||
MockCC.is_supported.return_value = True
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.exchange_code_for_tokens.side_effect = Exception("token error")
|
||||
MockCC.create_auth.return_value = mock_auth
|
||||
|
||||
resp = client.get(
|
||||
f"/api/connectors/callback?code=auth_code&state={state}"
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert "error" in resp.headers.get("Location", "")
|
||||
|
||||
def test_callback_bad_state_returns_error(self, client, mock_sessions):
|
||||
resp = client.get("/api/connectors/callback?code=auth_code&state=badbase64!!!")
|
||||
assert resp.status_code == 302
|
||||
assert "error" in resp.headers.get("Location", "")
|
||||
|
||||
def test_callback_user_info_fails_gracefully(self, client, mock_sessions):
|
||||
oid = mock_sessions["sessions"].insert_one(
|
||||
{
|
||||
"provider": "google_drive",
|
||||
"user": "test_user",
|
||||
"status": "pending",
|
||||
}
|
||||
).inserted_id
|
||||
state = self._encode_state(
|
||||
{"provider": "google_drive", "object_id": str(oid)}
|
||||
)
|
||||
with self._patch_connector_creator() as MockCC:
|
||||
MockCC.is_supported.return_value = True
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.exchange_code_for_tokens.return_value = {
|
||||
"access_token": "at",
|
||||
"refresh_token": "rt",
|
||||
}
|
||||
mock_auth.create_credentials_from_token_info.side_effect = Exception(
|
||||
"cred error"
|
||||
)
|
||||
mock_auth.sanitize_token_info.return_value = {
|
||||
"access_token": "at",
|
||||
}
|
||||
MockCC.create_auth.return_value = mock_auth
|
||||
|
||||
resp = client.get(
|
||||
f"/api/connectors/callback?code=auth_code&state={state}"
|
||||
)
|
||||
assert resp.status_code == 302
|
||||
assert "success" in resp.headers.get("Location", "")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConnectorFilesAdditional:
|
||||
"""Additional tests for ConnectorFiles."""
|
||||
|
||||
def test_unauthorized_user(self, client, mock_sessions):
|
||||
with patch("application.app.handle_auth", return_value=None):
|
||||
resp = client.post(
|
||||
"/api/connectors/files",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_files_with_pagination(self, client, mock_sessions):
|
||||
mock_sessions["sessions"].insert_one(
|
||||
{
|
||||
"session_token": "pag_tok",
|
||||
"user": "test_user",
|
||||
"provider": "google_drive",
|
||||
}
|
||||
)
|
||||
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.doc_id = "f1"
|
||||
mock_doc.extra_info = {
|
||||
"file_name": "test.pdf",
|
||||
"mime_type": "application/pdf",
|
||||
"size": 1024,
|
||||
"modified_time": "2025-01-01T12:00:00.000Z",
|
||||
"is_folder": False,
|
||||
}
|
||||
mock_loader = MagicMock()
|
||||
mock_loader.load_data.return_value = [mock_doc]
|
||||
mock_loader.next_page_token = "next_token_123"
|
||||
|
||||
with patch("application.api.connector.routes.ConnectorCreator") as MockCC:
|
||||
MockCC.create_connector.return_value = mock_loader
|
||||
resp = client.post(
|
||||
"/api/connectors/files",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "pag_tok",
|
||||
"page_token": "prev_token",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = json.loads(resp.data)
|
||||
assert data["has_more"] is True
|
||||
assert data["next_page_token"] == "next_token_123"
|
||||
|
||||
def test_files_exception_returns_500(self, client, mock_sessions):
|
||||
mock_sessions["sessions"].insert_one(
|
||||
{
|
||||
"session_token": "err_tok",
|
||||
"user": "test_user",
|
||||
"provider": "google_drive",
|
||||
}
|
||||
)
|
||||
|
||||
with patch("application.api.connector.routes.ConnectorCreator") as MockCC:
|
||||
MockCC.create_connector.side_effect = Exception("connector error")
|
||||
resp = client.post(
|
||||
"/api/connectors/files",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "err_tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 500
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConnectorFilesSearchQuery:
|
||||
"""Test ConnectorFiles with search_query parameter."""
|
||||
|
||||
def test_files_with_search_query(self, client, mock_sessions):
|
||||
mock_sessions["sessions"].insert_one(
|
||||
{
|
||||
"session_token": "search_tok",
|
||||
"user": "test_user",
|
||||
"provider": "google_drive",
|
||||
}
|
||||
)
|
||||
|
||||
mock_doc = MagicMock()
|
||||
mock_doc.doc_id = "f1"
|
||||
mock_doc.extra_info = {
|
||||
"file_name": "result.pdf",
|
||||
"mime_type": "application/pdf",
|
||||
"size": 512,
|
||||
"is_folder": False,
|
||||
}
|
||||
mock_loader = MagicMock()
|
||||
mock_loader.load_data.return_value = [mock_doc]
|
||||
mock_loader.next_page_token = None
|
||||
|
||||
with patch("application.api.connector.routes.ConnectorCreator") as MockCC:
|
||||
MockCC.create_connector.return_value = mock_loader
|
||||
resp = client.post(
|
||||
"/api/connectors/files",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "search_tok",
|
||||
"search_query": "test search",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = json.loads(resp.data)
|
||||
assert data["success"] is True
|
||||
# Verify search_query was passed in input_config
|
||||
call_args = mock_loader.load_data.call_args[0][0]
|
||||
assert call_args.get("search_query") == "test search"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Additional coverage tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConnectorValidateSessionAdditional:
|
||||
"""Cover uncovered branches in ConnectorValidateSession."""
|
||||
|
||||
def test_unauthorized_returns_401(self, client, mock_sessions):
|
||||
"""Line 288: decoded_token is None -> 401."""
|
||||
with patch("application.app.handle_auth", return_value=None):
|
||||
resp = client.post(
|
||||
"/api/connectors/validate-session",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_refresh_token_failure_still_expired(self, client, mock_sessions):
|
||||
"""Lines 299-310: refresh attempt fails, token stays expired."""
|
||||
mock_sessions["sessions"].insert_one({
|
||||
"session_token": "rf_fail_tok",
|
||||
"user": "test_user",
|
||||
"provider": "google_drive",
|
||||
"token_info": {
|
||||
"access_token": "old_at",
|
||||
"refresh_token": "rt",
|
||||
"expiry": 100,
|
||||
},
|
||||
})
|
||||
with patch("application.api.connector.routes.ConnectorCreator") as MockCC:
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.is_token_expired.return_value = True
|
||||
mock_auth.refresh_access_token.side_effect = Exception("refresh failed")
|
||||
MockCC.create_auth.return_value = mock_auth
|
||||
resp = client.post(
|
||||
"/api/connectors/validate-session",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "rf_fail_tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
data = json.loads(resp.data)
|
||||
assert data["expired"] is True
|
||||
|
||||
def test_provider_extras_in_response(self, client, mock_sessions):
|
||||
"""Lines 319-327: provider_extras are included in response."""
|
||||
mock_sessions["sessions"].insert_one({
|
||||
"session_token": "extras_tok",
|
||||
"user": "test_user",
|
||||
"provider": "google_drive",
|
||||
"token_info": {
|
||||
"access_token": "at",
|
||||
"refresh_token": "rt",
|
||||
"token_uri": "uri",
|
||||
"expiry": None,
|
||||
"custom_field": "custom_value",
|
||||
},
|
||||
"user_email": "user@test.com",
|
||||
})
|
||||
with patch("application.api.connector.routes.ConnectorCreator") as MockCC:
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.is_token_expired.return_value = False
|
||||
MockCC.create_auth.return_value = mock_auth
|
||||
resp = client.post(
|
||||
"/api/connectors/validate-session",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "extras_tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = json.loads(resp.data)
|
||||
assert data["success"] is True
|
||||
assert data["custom_field"] == "custom_value"
|
||||
assert data["user_email"] == "user@test.com"
|
||||
|
||||
def test_exception_returns_500(self, client, mock_sessions):
|
||||
"""Lines 331-333: general exception -> 500."""
|
||||
with patch("application.api.connector.routes.ConnectorCreator") as MockCC:
|
||||
MockCC.create_auth.side_effect = Exception("total failure")
|
||||
mock_sessions["sessions"].insert_one({
|
||||
"session_token": "err_tok",
|
||||
"user": "test_user",
|
||||
"provider": "google_drive",
|
||||
"token_info": {"access_token": "at"},
|
||||
})
|
||||
resp = client.post(
|
||||
"/api/connectors/validate-session",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "err_tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 500
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConnectorDisconnectAdditional:
|
||||
"""Cover uncovered branches in ConnectorDisconnect."""
|
||||
|
||||
def test_exception_returns_500(self, client, mock_sessions):
|
||||
"""Lines 353-355: exception in disconnect -> 500."""
|
||||
with patch(
|
||||
"application.api.connector.routes.sessions_collection"
|
||||
) as mock_col:
|
||||
mock_col.delete_one.side_effect = Exception("db down")
|
||||
resp = client.post(
|
||||
"/api/connectors/disconnect",
|
||||
json={
|
||||
"provider": "google_drive",
|
||||
"session_token": "tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 500
|
||||
|
||||
def test_unauthorized_still_works(self, client, mock_sessions):
|
||||
"""ConnectorDisconnect doesn't check decoded_token, just data parsing.
|
||||
No auth check branch to cover, but confirm basic flow."""
|
||||
resp = client.post(
|
||||
"/api/connectors/disconnect",
|
||||
json={"provider": "google_drive"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestConnectorSyncAdditional:
|
||||
"""Cover uncovered branches in ConnectorSync."""
|
||||
|
||||
def test_unauthorized_returns_401(self, client, mock_sessions):
|
||||
"""Line 373: decoded_token is None -> 401."""
|
||||
from bson.objectid import ObjectId as ObjId
|
||||
|
||||
with patch("application.app.handle_auth", return_value=None):
|
||||
resp = client.post(
|
||||
"/api/connectors/sync",
|
||||
json={
|
||||
"source_id": str(ObjId()),
|
||||
"session_token": "tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
def test_exception_returns_400(self, client, mock_sessions):
|
||||
"""Lines 453-464: general exception returns 400."""
|
||||
sid = mock_sessions["sources"].insert_one({
|
||||
"user": "test_user",
|
||||
"name": "src",
|
||||
"remote_data": json.dumps({
|
||||
"provider": "google_drive",
|
||||
"file_ids": ["f1"],
|
||||
}),
|
||||
}).inserted_id
|
||||
with patch(
|
||||
"application.api.connector.routes.ingest_connector_task"
|
||||
) as mock_ingest:
|
||||
mock_ingest.delay.side_effect = Exception("task error")
|
||||
resp = client.post(
|
||||
"/api/connectors/sync",
|
||||
json={
|
||||
"source_id": str(sid),
|
||||
"session_token": "tok",
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
def test_invalid_remote_data_json(self, client, mock_sessions):
|
||||
"""Line 411-413: invalid remote_data JSON."""
|
||||
sid = mock_sessions["sources"].insert_one({
|
||||
"user": "test_user",
|
||||
"name": "src",
|
||||
"remote_data": "not-valid-json{",
|
||||
}).inserted_id
|
||||
resp = client.post(
|
||||
"/api/connectors/sync",
|
||||
json={
|
||||
"source_id": str(sid),
|
||||
"session_token": "tok",
|
||||
},
|
||||
)
|
||||
# remote_data parsing fails, remote_data = {}, no provider -> 400
|
||||
assert resp.status_code == 400
|
||||
@@ -414,3 +414,166 @@ class TestUploadIndex:
|
||||
entry = db["sources"].find_one({"_id": ObjectId(doc_id)})
|
||||
assert entry["sync_frequency"] == "daily"
|
||||
assert entry["remote_data"] == '{"url":"http://example.com"}'
|
||||
|
||||
def test_faiss_upload_with_valid_files(self, internal_app, monkeypatch):
|
||||
"""Cover lines 93-104: FAISS upload with both faiss and pkl files."""
|
||||
app, db = internal_app
|
||||
doc_id = str(ObjectId())
|
||||
settings_mock = self._make_settings(vector_store="faiss")
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.settings", settings_mock
|
||||
)
|
||||
mock_storage = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.StorageCreator",
|
||||
MagicMock(get_storage=MagicMock(return_value=mock_storage)),
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
resp = client.post(
|
||||
"/api/upload_index",
|
||||
data={
|
||||
"user": "u",
|
||||
"name": "n",
|
||||
"tokens": "0",
|
||||
"retriever": "classic",
|
||||
"id": doc_id,
|
||||
"type": "local",
|
||||
"file_faiss": (io.BytesIO(b"faiss data"), "index.faiss"),
|
||||
"file_pkl": (io.BytesIO(b"pkl data"), "index.pkl"),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
)
|
||||
assert resp.json["status"] == "ok"
|
||||
|
||||
mock_storage.save_file.assert_called()
|
||||
entry = db["sources"].find_one({"_id": ObjectId(doc_id)})
|
||||
assert entry is not None
|
||||
|
||||
def test_faiss_pkl_missing_returns_no_file(self, internal_app, monkeypatch):
|
||||
"""Cover lines 93-95: FAISS upload with faiss file but no pkl file."""
|
||||
app, db = internal_app
|
||||
doc_id = str(ObjectId())
|
||||
settings_mock = self._make_settings(vector_store="faiss")
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.settings", settings_mock
|
||||
)
|
||||
mock_storage = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.StorageCreator",
|
||||
MagicMock(get_storage=MagicMock(return_value=mock_storage)),
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
resp = client.post(
|
||||
"/api/upload_index",
|
||||
data={
|
||||
"user": "u",
|
||||
"name": "n",
|
||||
"tokens": "0",
|
||||
"retriever": "classic",
|
||||
"id": doc_id,
|
||||
"type": "local",
|
||||
"file_faiss": (io.BytesIO(b"faiss data"), "index.faiss"),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
)
|
||||
assert resp.json["status"] == "no file"
|
||||
|
||||
def test_faiss_pkl_empty_name_returns_no_file_name(self, internal_app, monkeypatch):
|
||||
"""Cover lines 97-98: FAISS upload with pkl but empty filename."""
|
||||
app, db = internal_app
|
||||
doc_id = str(ObjectId())
|
||||
settings_mock = self._make_settings(vector_store="faiss")
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.settings", settings_mock
|
||||
)
|
||||
mock_storage = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.StorageCreator",
|
||||
MagicMock(get_storage=MagicMock(return_value=mock_storage)),
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
resp = client.post(
|
||||
"/api/upload_index",
|
||||
data={
|
||||
"user": "u",
|
||||
"name": "n",
|
||||
"tokens": "0",
|
||||
"retriever": "classic",
|
||||
"id": doc_id,
|
||||
"type": "local",
|
||||
"file_faiss": (io.BytesIO(b"faiss data"), "index.faiss"),
|
||||
"file_pkl": (io.BytesIO(b""), ""),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
)
|
||||
assert resp.json["status"] == "no file name"
|
||||
|
||||
def test_update_existing_with_file_name_map(self, internal_app, monkeypatch):
|
||||
"""Cover line 124: update existing entry with file_name_map."""
|
||||
app, db = internal_app
|
||||
doc_id = ObjectId()
|
||||
settings_mock = self._make_settings(vector_store="other")
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.settings", settings_mock
|
||||
)
|
||||
mock_storage = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.StorageCreator",
|
||||
MagicMock(get_storage=MagicMock(return_value=mock_storage)),
|
||||
)
|
||||
|
||||
db["sources"].insert_one({"_id": doc_id, "user": "old_user", "name": "old"})
|
||||
|
||||
fmap = {"hash1": "file1.txt"}
|
||||
with app.test_client() as client:
|
||||
resp = client.post(
|
||||
"/api/upload_index",
|
||||
data={
|
||||
"user": "u",
|
||||
"name": "n",
|
||||
"tokens": "0",
|
||||
"retriever": "classic",
|
||||
"id": str(doc_id),
|
||||
"type": "local",
|
||||
"file_name_map": json.dumps(fmap),
|
||||
},
|
||||
)
|
||||
assert resp.json["status"] == "ok"
|
||||
|
||||
entry = db["sources"].find_one({"_id": doc_id})
|
||||
assert entry["file_name_map"] == fmap
|
||||
|
||||
def test_invalid_file_name_map_defaults_none(self, internal_app, monkeypatch):
|
||||
"""Cover lines 77-79: invalid file_name_map JSON defaults to None."""
|
||||
app, db = internal_app
|
||||
doc_id = str(ObjectId())
|
||||
settings_mock = self._make_settings(vector_store="other")
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.settings", settings_mock
|
||||
)
|
||||
mock_storage = MagicMock()
|
||||
monkeypatch.setattr(
|
||||
"application.api.internal.routes.StorageCreator",
|
||||
MagicMock(get_storage=MagicMock(return_value=mock_storage)),
|
||||
)
|
||||
|
||||
with app.test_client() as client:
|
||||
resp = client.post(
|
||||
"/api/upload_index",
|
||||
data={
|
||||
"user": "u",
|
||||
"name": "n",
|
||||
"tokens": "0",
|
||||
"retriever": "classic",
|
||||
"id": doc_id,
|
||||
"type": "local",
|
||||
"file_name_map": "not valid json{{{",
|
||||
},
|
||||
)
|
||||
assert resp.json["status"] == "ok"
|
||||
|
||||
entry = db["sources"].find_one({"_id": ObjectId(doc_id)})
|
||||
assert "file_name_map" not in entry
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1336,3 +1336,388 @@ class TestTaskStatus:
|
||||
response = TaskStatus().get()
|
||||
|
||||
assert _status(response) == 400
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Additional coverage: zip extraction paths and ManageSourceFiles edge cases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUploadFileZipExtraction:
|
||||
"""Cover zip file extraction (lines 102-136) and error fallback."""
|
||||
|
||||
def test_zip_file_extraction_success(self, app):
|
||||
"""Lines 102-127: zip file is extracted and inner files uploaded."""
|
||||
import zipfile
|
||||
|
||||
from application.api.user.sources.upload import UploadFile
|
||||
|
||||
mock_storage = MagicMock()
|
||||
mock_task = SimpleNamespace(id="task-zip")
|
||||
|
||||
# Create a real zip file in memory
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("inner_file.txt", "hello zip content")
|
||||
zip_buffer.seek(0)
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/upload",
|
||||
method="POST",
|
||||
data={
|
||||
"user": "u1",
|
||||
"name": "ZipDoc",
|
||||
"file": (zip_buffer, "archive.zip"),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.ingest"
|
||||
) as mock_ingest, patch(
|
||||
"application.api.user.sources.upload._enforce_audio_path_size_limit",
|
||||
):
|
||||
mock_ingest.delay.return_value = mock_task
|
||||
response = UploadFile().post()
|
||||
|
||||
assert _status(response) == 200
|
||||
# Storage should have been called to save extracted files
|
||||
assert mock_storage.save_file.called
|
||||
|
||||
def test_zip_extraction_error_falls_back_to_original(self, app):
|
||||
"""Lines 128-136: zip extraction fails, original zip file is saved."""
|
||||
import zipfile
|
||||
|
||||
from application.api.user.sources.upload import UploadFile
|
||||
|
||||
mock_storage = MagicMock()
|
||||
mock_task = SimpleNamespace(id="task-zip-err")
|
||||
|
||||
# Create a real zip file in memory
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
zf.writestr("inner.txt", "content")
|
||||
zip_buffer.seek(0)
|
||||
|
||||
def bad_extractall(**kwargs):
|
||||
raise Exception("corrupt zip")
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/upload",
|
||||
method="POST",
|
||||
data={
|
||||
"user": "u1",
|
||||
"name": "BadZip",
|
||||
"file": (zip_buffer, "bad.zip"),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.ingest"
|
||||
) as mock_ingest, patch(
|
||||
"application.api.user.sources.upload._enforce_audio_path_size_limit",
|
||||
), patch(
|
||||
"application.api.user.sources.upload.zipfile.ZipFile"
|
||||
) as mock_zip_cls:
|
||||
mock_zip_instance = MagicMock()
|
||||
mock_zip_instance.__enter__ = MagicMock(return_value=mock_zip_instance)
|
||||
mock_zip_instance.__exit__ = MagicMock(return_value=False)
|
||||
mock_zip_instance.extractall.side_effect = Exception("corrupt zip")
|
||||
mock_zip_cls.return_value = mock_zip_instance
|
||||
mock_ingest.delay.return_value = mock_task
|
||||
response = UploadFile().post()
|
||||
|
||||
assert _status(response) == 200
|
||||
# Fallback: storage should save the original zip
|
||||
assert mock_storage.save_file.called
|
||||
|
||||
def test_upload_returns_413_for_oversized_audio(self, app):
|
||||
"""Lines 152-161: AudioFileTooLargeError caught."""
|
||||
from application.api.user.sources.upload import UploadFile
|
||||
from application.stt.upload_limits import AudioFileTooLargeError
|
||||
|
||||
mock_storage = MagicMock()
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/upload",
|
||||
method="POST",
|
||||
data={
|
||||
"user": "u1",
|
||||
"name": "AudioDoc",
|
||||
"file": (io.BytesIO(b"audio data"), "big.wav"),
|
||||
},
|
||||
content_type="multipart/form-data",
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.sources.upload._enforce_audio_path_size_limit",
|
||||
side_effect=AudioFileTooLargeError("too big"),
|
||||
):
|
||||
response = UploadFile().post()
|
||||
|
||||
assert _status(response) == 413
|
||||
assert "success" in _json(response) and _json(response)["success"] is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestManageSourceFilesAdditional:
|
||||
"""Additional edge cases for ManageSourceFiles."""
|
||||
|
||||
def test_remove_with_absolute_directory_path_rejected(self, app):
|
||||
"""Lines 513-523: directory_path starting with / is rejected."""
|
||||
from application.api.user.sources.upload import ManageSourceFiles
|
||||
|
||||
source_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": ObjectId(source_id),
|
||||
"user": "u1",
|
||||
"file_path": "uploads/u1/src",
|
||||
}
|
||||
mock_storage = MagicMock()
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/manage_source_files",
|
||||
method="POST",
|
||||
data={
|
||||
"source_id": source_id,
|
||||
"operation": "remove_directory",
|
||||
"directory_path": "/etc/passwd",
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSourceFiles().post()
|
||||
|
||||
assert _status(response) == 400
|
||||
assert "Invalid directory" in _json(response)["message"]
|
||||
|
||||
def test_remove_directory_no_keys_to_remove(self, app):
|
||||
"""Lines 564-577: remove_directory with file_name_map that has no
|
||||
matching keys (keys_to_remove is empty)."""
|
||||
from application.api.user.sources.upload import ManageSourceFiles
|
||||
|
||||
source_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": ObjectId(source_id),
|
||||
"user": "u1",
|
||||
"file_path": "uploads/u1/src",
|
||||
"file_name_map": {"unrelated.txt": "File.txt"},
|
||||
}
|
||||
mock_storage = MagicMock()
|
||||
mock_storage.is_directory.return_value = True
|
||||
mock_storage.remove_directory.return_value = True
|
||||
mock_task = SimpleNamespace(id="reingest-x")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.tasks.reingest_source_task"
|
||||
) as mock_reingest:
|
||||
mock_reingest.delay.return_value = mock_task
|
||||
with app.test_request_context(
|
||||
"/api/manage_source_files",
|
||||
method="POST",
|
||||
data={
|
||||
"source_id": source_id,
|
||||
"operation": "remove_directory",
|
||||
"directory_path": "no_match_dir",
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSourceFiles().post()
|
||||
|
||||
assert _status(response) == 200
|
||||
# update_one should NOT be called because no keys matched
|
||||
mock_collection.update_one.assert_not_called()
|
||||
|
||||
def test_general_error_remove_directory_context(self, app):
|
||||
"""Line 598-600: error context includes directory_path for
|
||||
remove_directory operation."""
|
||||
from application.api.user.sources.upload import ManageSourceFiles
|
||||
|
||||
source_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": ObjectId(source_id),
|
||||
"user": "u1",
|
||||
"file_path": "uploads/u1/src",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
side_effect=Exception("storage crash"),
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/manage_source_files",
|
||||
method="POST",
|
||||
data={
|
||||
"source_id": source_id,
|
||||
"operation": "remove_directory",
|
||||
"directory_path": "mydir",
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSourceFiles().post()
|
||||
|
||||
assert _status(response) == 500
|
||||
assert "Operation failed" in _json(response)["message"]
|
||||
|
||||
def test_general_error_add_context(self, app):
|
||||
"""Lines 604-606: error context includes parent_dir for add operation."""
|
||||
from application.api.user.sources.upload import ManageSourceFiles
|
||||
|
||||
source_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": ObjectId(source_id),
|
||||
"user": "u1",
|
||||
"file_path": "uploads/u1/src",
|
||||
}
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
side_effect=Exception("storage crash"),
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/manage_source_files",
|
||||
method="POST",
|
||||
data={
|
||||
"source_id": source_id,
|
||||
"operation": "add",
|
||||
"parent_dir": "sub",
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSourceFiles().post()
|
||||
|
||||
assert _status(response) == 500
|
||||
|
||||
def test_file_name_map_non_dict_reset(self, app):
|
||||
"""Lines 366-367: file_name_map not a dict is reset to {}."""
|
||||
from application.api.user.sources.upload import ManageSourceFiles
|
||||
|
||||
source_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": ObjectId(source_id),
|
||||
"user": "u1",
|
||||
"file_path": "uploads/u1/src",
|
||||
"file_name_map": [1, 2, 3], # not a dict
|
||||
}
|
||||
mock_storage = MagicMock()
|
||||
mock_storage.file_exists.return_value = True
|
||||
mock_task = SimpleNamespace(id="reingest-nd")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.tasks.reingest_source_task"
|
||||
) as mock_reingest:
|
||||
mock_reingest.delay.return_value = mock_task
|
||||
with app.test_request_context(
|
||||
"/api/manage_source_files",
|
||||
method="POST",
|
||||
data={
|
||||
"source_id": source_id,
|
||||
"operation": "remove",
|
||||
"file_paths": json.dumps(["x.txt"]),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSourceFiles().post()
|
||||
|
||||
assert _status(response) == 200
|
||||
|
||||
def test_file_name_map_invalid_json_string_reset(self, app):
|
||||
"""Lines 362-365: file_name_map is a string but not valid JSON."""
|
||||
from application.api.user.sources.upload import ManageSourceFiles
|
||||
|
||||
source_id = str(ObjectId())
|
||||
mock_collection = Mock()
|
||||
mock_collection.find_one.return_value = {
|
||||
"_id": ObjectId(source_id),
|
||||
"user": "u1",
|
||||
"file_path": "uploads/u1/src",
|
||||
"file_name_map": "not-valid-json{",
|
||||
}
|
||||
mock_storage = MagicMock()
|
||||
mock_storage.file_exists.return_value = False
|
||||
mock_task = SimpleNamespace(id="reingest-ij")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sources.upload.sources_collection",
|
||||
mock_collection,
|
||||
), patch(
|
||||
"application.api.user.sources.upload.StorageCreator.get_storage",
|
||||
return_value=mock_storage,
|
||||
), patch(
|
||||
"application.api.user.tasks.reingest_source_task"
|
||||
) as mock_reingest:
|
||||
mock_reingest.delay.return_value = mock_task
|
||||
with app.test_request_context(
|
||||
"/api/manage_source_files",
|
||||
method="POST",
|
||||
data={
|
||||
"source_id": source_id,
|
||||
"operation": "remove",
|
||||
"file_paths": json.dumps(["x.txt"]),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "u1"}
|
||||
response = ManageSourceFiles().post()
|
||||
|
||||
assert _status(response) == 200
|
||||
File diff suppressed because it is too large.
Load diff
@@ -386,3 +386,514 @@ class TestGetUserLogs:
|
||||
response = GetUserLogs().post()
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetTokenAnalyticsAdditional:
|
||||
"""Additional tests for GetTokenAnalytics covering missing lines."""
|
||||
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.analytics.routes import GetTokenAnalytics
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/get_token_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_30_days"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = GetTokenAnalytics().post()
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_last_hour_filter(self, app):
|
||||
from application.api.user.analytics.routes import GetTokenAnalytics
|
||||
|
||||
mock_token_usage = Mock()
|
||||
mock_token_usage.aggregate.return_value = [
|
||||
{"_id": {"minute": "2024-06-01 12:00:00"}, "total_tokens": 500}
|
||||
]
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.token_usage_collection",
|
||||
mock_token_usage,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_token_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_hour"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetTokenAnalytics().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json["success"] is True
|
||||
assert "token_usage" in response.json
|
||||
|
||||
def test_last_24_hour_filter(self, app):
|
||||
from application.api.user.analytics.routes import GetTokenAnalytics
|
||||
|
||||
mock_token_usage = Mock()
|
||||
mock_token_usage.aggregate.return_value = [
|
||||
{"_id": {"hour": "2024-06-01 12:00"}, "total_tokens": 800}
|
||||
]
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.token_usage_collection",
|
||||
mock_token_usage,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_token_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_24_hour"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetTokenAnalytics().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json["success"] is True
|
||||
|
||||
def test_filters_by_api_key(self, app):
|
||||
from application.api.user.analytics.routes import GetTokenAnalytics
|
||||
|
||||
agent_id = ObjectId()
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = {
|
||||
"_id": agent_id,
|
||||
"key": "token_api_key",
|
||||
}
|
||||
mock_token_usage = Mock()
|
||||
mock_token_usage.aggregate.return_value = []
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.token_usage_collection",
|
||||
mock_token_usage,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_token_analytics",
|
||||
method="POST",
|
||||
json={
|
||||
"filter_option": "last_7_days",
|
||||
"api_key_id": str(agent_id),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetTokenAnalytics().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
pipeline = mock_token_usage.aggregate.call_args[0][0]
|
||||
assert pipeline[0]["$match"].get("api_key") == "token_api_key"
|
||||
|
||||
def test_api_key_error_returns_400(self, app):
|
||||
from application.api.user.analytics.routes import GetTokenAnalytics
|
||||
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_token_analytics",
|
||||
method="POST",
|
||||
json={
|
||||
"filter_option": "last_30_days",
|
||||
"api_key_id": str(ObjectId()),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetTokenAnalytics().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_aggregate_error_returns_400(self, app):
|
||||
from application.api.user.analytics.routes import GetTokenAnalytics
|
||||
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
mock_token_usage = Mock()
|
||||
mock_token_usage.aggregate.side_effect = Exception("aggregate error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.token_usage_collection",
|
||||
mock_token_usage,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_token_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_30_days"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetTokenAnalytics().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_last_15_days_filter(self, app):
|
||||
from application.api.user.analytics.routes import GetTokenAnalytics
|
||||
|
||||
mock_token_usage = Mock()
|
||||
mock_token_usage.aggregate.return_value = []
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.token_usage_collection",
|
||||
mock_token_usage,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_token_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_15_days"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetTokenAnalytics().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetFeedbackAnalyticsAdditional:
|
||||
"""Additional tests for GetFeedbackAnalytics covering missing lines."""
|
||||
|
||||
def test_returns_401_unauthenticated(self, app):
|
||||
from application.api.user.analytics.routes import GetFeedbackAnalytics
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/get_feedback_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_30_days"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = GetFeedbackAnalytics().post()
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_last_hour_filter(self, app):
|
||||
from application.api.user.analytics.routes import GetFeedbackAnalytics
|
||||
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.aggregate.return_value = []
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_feedback_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_hour"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetFeedbackAnalytics().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_last_24_hour_filter(self, app):
|
||||
from application.api.user.analytics.routes import GetFeedbackAnalytics
|
||||
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.aggregate.return_value = []
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_feedback_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_24_hour"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetFeedbackAnalytics().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_filters_by_api_key(self, app):
|
||||
from application.api.user.analytics.routes import GetFeedbackAnalytics
|
||||
|
||||
agent_id = ObjectId()
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = {
|
||||
"_id": agent_id,
|
||||
"key": "fb_api_key",
|
||||
}
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.aggregate.return_value = []
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_feedback_analytics",
|
||||
method="POST",
|
||||
json={
|
||||
"filter_option": "last_7_days",
|
||||
"api_key_id": str(agent_id),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetFeedbackAnalytics().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
pipeline = mock_conversations.aggregate.call_args[0][0]
|
||||
assert pipeline[0]["$match"].get("api_key") == "fb_api_key"
|
||||
|
||||
def test_api_key_error_returns_400(self, app):
|
||||
from application.api.user.analytics.routes import GetFeedbackAnalytics
|
||||
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_feedback_analytics",
|
||||
method="POST",
|
||||
json={
|
||||
"filter_option": "last_30_days",
|
||||
"api_key_id": str(ObjectId()),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetFeedbackAnalytics().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_aggregate_error_returns_400(self, app):
|
||||
from application.api.user.analytics.routes import GetFeedbackAnalytics
|
||||
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.aggregate.side_effect = Exception("aggregate error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_feedback_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_30_days"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetFeedbackAnalytics().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetMessageAnalyticsAdditional:
|
||||
"""Additional tests for GetMessageAnalytics covering error paths."""
|
||||
|
||||
def test_api_key_error_returns_400(self, app):
|
||||
from application.api.user.analytics.routes import GetMessageAnalytics
|
||||
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_message_analytics",
|
||||
method="POST",
|
||||
json={
|
||||
"filter_option": "last_30_days",
|
||||
"api_key_id": str(ObjectId()),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetMessageAnalytics().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_aggregate_error_returns_400(self, app):
|
||||
from application.api.user.analytics.routes import GetMessageAnalytics
|
||||
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.aggregate.side_effect = Exception("aggregate error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_message_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_30_days"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetMessageAnalytics().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_last_15_days_filter(self, app):
|
||||
from application.api.user.analytics.routes import GetMessageAnalytics
|
||||
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.aggregate.return_value = []
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = None
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_message_analytics",
|
||||
method="POST",
|
||||
json={"filter_option": "last_15_days"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetMessageAnalytics().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetUserLogsAdditional:
|
||||
"""Additional tests for GetUserLogs covering api_key filtering and errors."""
|
||||
|
||||
def test_filters_by_api_key(self, app):
|
||||
from application.api.user.analytics.routes import GetUserLogs
|
||||
|
||||
agent_id = ObjectId()
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.return_value = {
|
||||
"_id": agent_id,
|
||||
"key": "logs_api_key",
|
||||
}
|
||||
mock_cursor = Mock()
|
||||
mock_cursor.sort.return_value.skip.return_value.limit.return_value = []
|
||||
mock_user_logs = Mock()
|
||||
mock_user_logs.find.return_value = mock_cursor
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.user_logs_collection",
|
||||
mock_user_logs,
|
||||
), patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_user_logs",
|
||||
method="POST",
|
||||
json={
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"api_key_id": str(agent_id),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetUserLogs().post()
|
||||
|
||||
assert response.status_code == 200
|
||||
query_arg = mock_user_logs.find.call_args[0][0]
|
||||
assert query_arg == {"api_key": "logs_api_key"}
|
||||
|
||||
def test_api_key_error_returns_400(self, app):
|
||||
from application.api.user.analytics.routes import GetUserLogs
|
||||
|
||||
mock_agents = Mock()
|
||||
mock_agents.find_one.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.analytics.routes.agents_collection",
|
||||
mock_agents,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/get_user_logs",
|
||||
method="POST",
|
||||
json={
|
||||
"page": 1,
|
||||
"api_key_id": str(ObjectId()),
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = GetUserLogs().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
@@ -507,3 +507,116 @@ class TestBulkMoveAgents:
|
||||
response = BulkMoveAgents().post()
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Coverage gap tests (lines 64, 90-91, 100, 125-126, 132, 136)
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAgentFoldersGaps:
|
||||
|
||||
def test_create_folder_no_auth(self, app):
|
||||
"""Cover line 64: post returns 401 when no decoded_token."""
|
||||
from application.api.user.agents.folders import AgentFolders
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/agents/folders/",
|
||||
method="POST",
|
||||
json={"name": "Test"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = AgentFolders().post()
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_create_folder_exception(self, app):
|
||||
"""Cover lines 90-91: exception during insert_one returns 400."""
|
||||
from application.api.user.agents.folders import AgentFolders
|
||||
|
||||
mock_folders = Mock()
|
||||
mock_folders.find_one.return_value = None
|
||||
mock_folders.insert_one.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.agents.folders.agent_folders_collection",
|
||||
mock_folders,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/agents/folders/",
|
||||
method="POST",
|
||||
json={"name": "Test"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = AgentFolders().post()
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_get_folder_no_auth(self, app):
|
||||
"""Cover line 100: get specific folder returns 401 when no auth."""
|
||||
from application.api.user.agents.folders import AgentFolder
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/agents/folders/abc",
|
||||
method="GET",
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = AgentFolder().get("abc")
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_get_folder_exception(self, app):
|
||||
"""Cover lines 125-126: exception during find returns 400."""
|
||||
from application.api.user.agents.folders import AgentFolder
|
||||
|
||||
mock_folders = Mock()
|
||||
mock_folders.find_one.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.agents.folders.agent_folders_collection",
|
||||
mock_folders,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/agents/folders/" + str(ObjectId()),
|
||||
method="GET",
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = AgentFolder().get(str(ObjectId()))
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_update_folder_no_auth(self, app):
|
||||
"""Cover line 132: put returns 401 when no decoded_token."""
|
||||
from application.api.user.agents.folders import AgentFolder
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/agents/folders/abc",
|
||||
method="PUT",
|
||||
json={"name": "Updated"},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = None
|
||||
response = AgentFolder().put("abc")
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_update_folder_no_data(self, app):
|
||||
"""Cover line 136: put with no data returns 400."""
|
||||
from application.api.user.agents.folders import AgentFolder
|
||||
|
||||
with app.test_request_context(
|
||||
"/api/agents/folders/abc",
|
||||
method="PUT",
|
||||
content_type="application/json",
|
||||
data="null",
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = AgentFolder().put("abc")
|
||||
assert response.status_code == 400
|
||||
@@ -688,3 +688,116 @@ class TestShareConversationPromptable:
|
||||
assert response.status_code == 201
|
||||
mock_agents.insert_one.assert_called_once()
|
||||
mock_shared.insert_one.assert_called_once()
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# Coverage gap tests (lines 201-205)
|
||||
# =====================================================================
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestShareConversationExceptionGap:
|
||||
def test_share_conversation_exception_returns_400(self):
|
||||
"""Cover lines 201-205: exception during sharing returns 400."""
|
||||
from application.api.user.sharing.routes import ShareConversation
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.find_one.side_effect = Exception("db error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sharing.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/share",
|
||||
method="POST",
|
||||
json={
|
||||
"conversation_id": str(ObjectId()),
|
||||
"source": str(ObjectId()),
|
||||
"retriever": "classic",
|
||||
},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = ShareConversation().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Coverage — additional uncovered lines: 201-205
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestShareConversationErrorPath:
|
||||
|
||||
def test_share_conversation_exception_returns_400(self, app):
|
||||
"""Cover lines 201-205: exception during sharing returns 400."""
|
||||
from application.api.user.sharing.routes import ShareConversation
|
||||
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.find_one.side_effect = Exception("DB error")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sharing.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/share",
|
||||
method="POST",
|
||||
json={"conversation_id": str(ObjectId())},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = ShareConversation().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Additional coverage for sharing/routes.py
|
||||
# Lines: 201-205: exception in try block (different entry point)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestShareConversationInsertException:
|
||||
"""Cover lines 201-205: exception during insert_one."""
|
||||
|
||||
def test_insert_one_exception_returns_400(self, app):
|
||||
from application.api.user.sharing.routes import ShareConversation
|
||||
|
||||
mock_conversations = Mock()
|
||||
mock_conversations.find_one.return_value = {
|
||||
"_id": ObjectId(),
|
||||
"user": "user1",
|
||||
"queries": [],
|
||||
}
|
||||
mock_shared = Mock()
|
||||
mock_shared.find_one.return_value = None
|
||||
mock_shared.insert_one.side_effect = Exception("Insert failed")
|
||||
|
||||
with patch(
|
||||
"application.api.user.sharing.routes.conversations_collection",
|
||||
mock_conversations,
|
||||
), patch(
|
||||
"application.api.user.sharing.routes.shared_conversations_collections",
|
||||
mock_shared,
|
||||
):
|
||||
with app.test_request_context(
|
||||
"/api/share",
|
||||
method="POST",
|
||||
json={"conversation_id": str(ObjectId())},
|
||||
):
|
||||
from flask import request
|
||||
|
||||
request.decoded_token = {"sub": "user1"}
|
||||
response = ShareConversation().post()
|
||||
|
||||
assert response.status_code == 400
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user