chore: more tests

This commit is contained in:
Alex committed 2026-03-30 16:13:08 +01:00
1 parent dc6db847ca
commit d5c0322e2a
64 files changed
+24436 -140

No files matched your search

@@ -0,0 +1,46 @@
from unittest.mock import patch
import pytest
@pytest.mark.unit
class TestCompressionThresholdChecker:
def _make_checker(self, pct=0.7):
from application.api.answer.services.compression.threshold_checker import (
CompressionThresholdChecker,
)
return CompressionThresholdChecker(threshold_percentage=pct)
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=8000,
)
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
return_value=6000,
)
def test_check_message_tokens_above_threshold(self, mock_count, mock_limit):
checker = self._make_checker(0.7)
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is True
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=8000,
)
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
return_value=1000,
)
def test_check_message_tokens_below_threshold(self, mock_count, mock_limit):
checker = self._make_checker(0.7)
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is False
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
side_effect=Exception("Token error"),
)
def test_check_message_tokens_exception_returns_false(self, mock_count):
checker = self._make_checker(0.7)
assert checker.check_message_tokens([], "gpt-4") is False
+363
View File
@@ -0,0 +1,363 @@
"""Unit tests for application/api/answer/routes/base.py — BaseAnswerResource.
Additional coverage beyond tests/api/answer/routes/test_base.py:
- _prepare_tool_calls_for_logging: truncation, non-dict items
- complete_stream: tool_calls, thoughts, structured output, metadata,
isNoneDoc, GeneratorExit handling, compression metadata
- process_response_stream: structured answer, incomplete stream
- error_stream_generate: format
- check_usage: string boolean parsing ("True" strings)
"""
import json
from unittest.mock import MagicMock
import pytest
from bson import ObjectId
@pytest.mark.unit
class TestPrepareToolCallsForLogging:
def test_empty_list(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
assert resource._prepare_tool_calls_for_logging([]) == []
def test_none_returns_empty(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
assert resource._prepare_tool_calls_for_logging(None) == []
def test_truncates_long_result(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
tool_calls = [{"result": "x" * 20000}]
prepared = resource._prepare_tool_calls_for_logging(tool_calls, max_chars=100)
assert len(prepared[0]["result"]) == 100
def test_truncates_result_full(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
tool_calls = [{"result_full": "y" * 20000}]
prepared = resource._prepare_tool_calls_for_logging(tool_calls, max_chars=50)
assert len(prepared[0]["result_full"]) == 50
def test_non_dict_items_wrapped(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
tool_calls = ["string_item", 42]
prepared = resource._prepare_tool_calls_for_logging(tool_calls)
assert prepared[0] == {"result": "string_item"}
assert prepared[1] == {"result": "42"}
def test_preserves_short_results(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
tool_calls = [{"tool_name": "search", "result": "short text"}]
prepared = resource._prepare_tool_calls_for_logging(tool_calls)
assert prepared[0]["result"] == "short text"
assert prepared[0]["tool_name"] == "search"
@pytest.mark.unit
class TestCompleteStreamToolCalls:
def test_streams_tool_calls(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": "Using tool..."},
{"tool_calls": [{"name": "search", "result": "found"}]},
]
)
stream = list(
resource.complete_stream(
question="Search for X",
agent=mock_agent,
conversation_id=None,
user_api_key=None,
decoded_token={"sub": "u"},
should_save_conversation=False,
)
)
tool_chunks = [s for s in stream if '"type": "tool_calls"' in s]
assert len(tool_chunks) == 1
def test_streams_thought_events(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(
[
{"thought": "Let me think..."},
{"answer": "Here is the answer"},
]
)
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,
)
)
thought_chunks = [s for s in stream if '"type": "thought"' in s]
assert len(thought_chunks) == 1
assert "Let me think" in thought_chunks[0]
@pytest.mark.unit
class TestCompleteStreamStructuredOutput:
def test_streams_structured_answer(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": '{"key": "value"}',
"structured": True,
"schema": {"type": "object"},
},
]
)
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,
)
)
structured_chunks = [
s for s in stream if '"type": "structured_answer"' in s
]
assert len(structured_chunks) == 1
@pytest.mark.unit
class TestCompleteStreamMetadata:
def test_metadata_collected(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(
[
{"metadata": {"search_query": "test"}},
{"answer": "result"},
]
)
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,
)
)
# Should not crash, metadata handled silently
answer_chunks = [s for s in stream if '"type": "answer"' in s]
assert len(answer_chunks) == 1
@pytest.mark.unit
class TestCompleteStreamIsNoneDoc:
def test_isNoneDoc_sets_source_to_none(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": "answer"},
{"sources": [{"text": "doc", "source": "real_source"}]},
]
)
stream = list(
resource.complete_stream(
question="Q",
agent=mock_agent,
conversation_id=None,
user_api_key=None,
decoded_token={"sub": "u"},
isNoneDoc=True,
should_save_conversation=False,
)
)
# Verify stream completes without error
end_chunks = [s for s in stream if '"type": "end"' in s]
assert len(end_chunks) == 1
@pytest.mark.unit
class TestCompleteStreamErrorType:
def test_error_type_event_sanitized(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(
[
{"type": "error", "error": "API key invalid: sk-xxx"},
]
)
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,
)
)
error_chunks = [s for s in stream if '"type": "error"' in s]
assert len(error_chunks) == 1
def test_non_error_type_event_passed_through(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(
[
{"type": "custom_event", "data": "value"},
]
)
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,
)
)
custom_chunks = [s for s in stream if '"type": "custom_event"' in s]
assert len(custom_chunks) == 1
@pytest.mark.unit
class TestProcessResponseStreamExtended:
def test_handles_structured_answer(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
stream = [
f'data: {json.dumps({"type": "structured_answer", "answer": "{}", "structured": True, "schema": None})}\n\n',
f'data: {json.dumps({"type": "id", "id": str(ObjectId())})}\n\n',
f'data: {json.dumps({"type": "end"})}\n\n',
]
result = resource.process_response_stream(iter(stream))
assert result[1] == "{}"
# Structured output adds extra tuple element
assert len(result) == 7
assert result[6]["structured"] is True
def test_handles_tool_calls_event(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
stream = [
f'data: {json.dumps({"type": "answer", "answer": "result"})}\n\n',
f'data: {json.dumps({"type": "tool_calls", "tool_calls": [{"name": "t1"}]})}\n\n',
f'data: {json.dumps({"type": "id", "id": "conv1"})}\n\n',
f'data: {json.dumps({"type": "end"})}\n\n',
]
result = resource.process_response_stream(iter(stream))
assert result[3] == [{"name": "t1"}]
def test_incomplete_stream(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
stream = [
f'data: {json.dumps({"type": "answer", "answer": "partial"})}\n\n',
]
result = resource.process_response_stream(iter(stream))
assert result[4] == "Stream ended unexpectedly"
def test_handles_thought_event(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
with flask_app.app_context():
resource = BaseAnswerResource()
stream = [
f'data: {json.dumps({"type": "thought", "thought": "thinking..."})}\n\n',
f'data: {json.dumps({"type": "end"})}\n\n',
]
result = resource.process_response_stream(iter(stream))
assert result[4] == "thinking..."
@pytest.mark.unit
class TestCheckUsageStringBooleans:
def test_string_true_parsed_correctly(self, mock_mongo_db, flask_app):
from application.api.answer.routes.base import BaseAnswerResource
from application.core.settings import settings
with flask_app.app_context():
agents_collection = mock_mongo_db[settings.MONGO_DB_NAME]["agents"]
agents_collection.insert_one(
{
"_id": ObjectId(),
"key": "str_bool_key",
"limited_token_mode": "True",
"token_limit": 1000000,
"limited_request_mode": "True",
"request_limit": 1000000,
}
)
resource = BaseAnswerResource()
result = resource.check_usage({"user_api_key": "str_bool_key"})
# Should not exceed limits, so returns None
assert result is None
@@ -0,0 +1,418 @@
"""Unit tests for application/api/answer/services/conversation_service.py.
Additional coverage beyond tests/api/answer/services/test_conversation_service.py:
- save_conversation: index-based update, metadata persistence, agent key tracking
- update_compression_metadata
- append_compression_message
- get_compression_metadata
- Edge cases: None token, empty summary, shared_with access
"""
from datetime import datetime, timezone
from unittest.mock import Mock
import pytest
from bson import ObjectId
@pytest.mark.unit
class TestConversationServiceGetExtended:
def test_returns_conversation_for_shared_user(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
conv_id = ObjectId()
collection.insert_one(
{
"_id": conv_id,
"user": "owner_123",
"shared_with": ["shared_user"],
"name": "Shared Conv",
"queries": [],
}
)
result = service.get_conversation(str(conv_id), "shared_user")
assert result is not None
assert result["name"] == "Shared Conv"
def test_handles_exception_gracefully(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
service = ConversationService()
# Pass an invalid ObjectId
result = service.get_conversation("not-an-objectid", "user_123")
assert result is None
@pytest.mark.unit
class TestSaveConversationExtended:
def test_raises_for_none_token(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
service = ConversationService()
with pytest.raises(ValueError, match="Invalid or missing authentication"):
service.save_conversation(
conversation_id=None,
question="Q",
response="A",
thought="",
sources=[],
tool_calls=[],
llm=Mock(),
model_id="m",
decoded_token=None,
)
def test_update_existing_at_index(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
conv_id = ObjectId()
collection.insert_one(
{
"_id": conv_id,
"user": "user_123",
"name": "Conv",
"queries": [
{
"prompt": "Q1",
"response": "A1",
"thought": "",
"sources": [],
"tool_calls": [],
},
{
"prompt": "Q2",
"response": "A2",
"thought": "",
"sources": [],
"tool_calls": [],
},
],
}
)
result = service.save_conversation(
conversation_id=str(conv_id),
question="Q1_updated",
response="A1_updated",
thought="thinking",
sources=[],
tool_calls=[],
llm=Mock(),
model_id="gpt-4",
decoded_token={"sub": "user_123"},
index=0,
)
assert result == str(conv_id)
saved = collection.find_one({"_id": conv_id})
assert saved["queries"][0]["prompt"] == "Q1_updated"
assert saved["queries"][0]["response"] == "A1_updated"
def test_update_at_index_unauthorized(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
conv_id = ObjectId()
collection.insert_one(
{
"_id": conv_id,
"user": "owner",
"queries": [{"prompt": "Q", "response": "A"}],
}
)
with pytest.raises(ValueError, match="not found or unauthorized"):
service.save_conversation(
conversation_id=str(conv_id),
question="Hack",
response="Attempt",
thought="",
sources=[],
tool_calls=[],
llm=Mock(),
model_id="m",
decoded_token={"sub": "hacker"},
index=0,
)
def test_saves_metadata(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
mock_llm = Mock()
mock_llm.gen.return_value = "Title"
conv_id = service.save_conversation(
conversation_id=None,
question="Q",
response="A",
thought="",
sources=[],
tool_calls=[],
llm=mock_llm,
model_id="m",
decoded_token={"sub": "user_123"},
metadata={"search_query": "rewritten query"},
)
saved = collection.find_one({"_id": ObjectId(conv_id)})
assert saved["queries"][0]["metadata"] == {"search_query": "rewritten query"}
def test_no_metadata_when_none(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
mock_llm = Mock()
mock_llm.gen.return_value = "Title"
conv_id = service.save_conversation(
conversation_id=None,
question="Q",
response="A",
thought="",
sources=[],
tool_calls=[],
llm=mock_llm,
model_id="m",
decoded_token={"sub": "user_123"},
metadata=None,
)
saved = collection.find_one({"_id": ObjectId(conv_id)})
assert "metadata" not in saved["queries"][0]
def test_saves_with_api_key_and_agent(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
agents_collection = mock_mongo_db[settings.MONGO_DB_NAME]["agents"]
agent_id = ObjectId()
agents_collection.insert_one(
{"_id": agent_id, "key": "agent_key_123", "user": "user_123"}
)
mock_llm = Mock()
mock_llm.gen.return_value = "Title"
conv_id = service.save_conversation(
conversation_id=None,
question="Q",
response="A",
thought="",
sources=[],
tool_calls=[],
llm=mock_llm,
model_id="m",
decoded_token={"sub": "user_123"},
api_key="agent_key_123",
agent_id=str(agent_id),
)
saved = collection.find_one({"_id": ObjectId(conv_id)})
assert saved["api_key"] == "agent_key_123"
assert saved["agent_id"] == str(agent_id)
def test_empty_completion_uses_question_prefix(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
mock_llm = Mock()
mock_llm.gen.return_value = " " # whitespace only
conv_id = service.save_conversation(
conversation_id=None,
question="What is the meaning of life in programming?",
response="42",
thought="",
sources=[],
tool_calls=[],
llm=mock_llm,
model_id="m",
decoded_token={"sub": "user_123"},
)
saved = collection.find_one({"_id": ObjectId(conv_id)})
assert saved["name"] == "What is the meaning of life in programming?"[:50]
@pytest.mark.unit
class TestUpdateCompressionMetadata:
def test_updates_compression_fields(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
conv_id = ObjectId()
collection.insert_one(
{"_id": conv_id, "user": "u", "queries": []}
)
meta = {
"timestamp": datetime.now(timezone.utc),
"compressed_summary": "Summary of conversation",
"model_used": "gpt-4",
}
service.update_compression_metadata(str(conv_id), meta)
saved = collection.find_one({"_id": conv_id})
assert saved["compression_metadata"]["is_compressed"] is True
assert len(saved["compression_metadata"]["compression_points"]) == 1
@pytest.mark.unit
class TestAppendCompressionMessage:
def test_appends_summary_query(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
conv_id = ObjectId()
collection.insert_one(
{"_id": conv_id, "user": "u", "queries": []}
)
meta = {
"compressed_summary": "This is the summary",
"timestamp": datetime.now(timezone.utc),
"model_used": "gpt-4",
}
service.append_compression_message(str(conv_id), meta)
saved = collection.find_one({"_id": conv_id})
assert len(saved["queries"]) == 1
assert saved["queries"][0]["prompt"] == "[Context Compression Summary]"
assert saved["queries"][0]["response"] == "This is the summary"
def test_empty_summary_does_nothing(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
conv_id = ObjectId()
collection.insert_one(
{"_id": conv_id, "user": "u", "queries": []}
)
service.append_compression_message(str(conv_id), {"compressed_summary": ""})
saved = collection.find_one({"_id": conv_id})
assert len(saved["queries"]) == 0
@pytest.mark.unit
class TestGetCompressionMetadata:
def test_returns_metadata(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
conv_id = ObjectId()
collection.insert_one(
{
"_id": conv_id,
"user": "u",
"compression_metadata": {"is_compressed": True},
}
)
result = service.get_compression_metadata(str(conv_id))
assert result is not None
assert result["is_compressed"] is True
def test_returns_none_for_no_metadata(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
from application.core.settings import settings
service = ConversationService()
collection = mock_mongo_db[settings.MONGO_DB_NAME]["conversations"]
conv_id = ObjectId()
collection.insert_one({"_id": conv_id, "user": "u"})
result = service.get_compression_metadata(str(conv_id))
assert result is None
def test_returns_none_for_missing_conversation(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
service = ConversationService()
result = service.get_compression_metadata(str(ObjectId()))
assert result is None
def test_handles_invalid_id(self, mock_mongo_db):
from application.api.answer.services.conversation_service import (
ConversationService,
)
service = ConversationService()
result = service.get_compression_metadata("invalid-id")
assert result is None
+184 -1
View File
@@ -1,4 +1,17 @@
"""Tests for application/api/answer/services/stream_processor.py — get_prompt and helpers."""
"""Tests for application/api/answer/services/stream_processor.py — get_prompt and helpers.
Extended coverage for StreamProcessor including:
- get_prompt: all presets and DB fallback
- StreamProcessor init, _resolve_agent_id, _get_prompt_content
- _get_required_tool_actions
- _get_attachments_content: valid, invalid, empty
- _configure_retriever
- _validate_and_set_model
- _get_agent_key
- _get_data_from_api_key
- _configure_source
- pre_fetch_docs
"""
from unittest.mock import MagicMock, patch
@@ -119,6 +132,22 @@ class TestStreamProcessorInit:
sp = StreamProcessor(request_data={}, decoded_token=None)
assert sp.initial_user_id is None
@pytest.mark.unit
def test_init_default_model_and_config(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(request_data={}, decoded_token={"sub": "u"})
assert sp.model_id is None
assert sp.is_shared_usage is False
assert sp.shared_token is None
assert sp.compressed_summary is None
assert sp.compressed_summary_tokens == 0
class TestGetAttachmentsContent:
@@ -169,6 +198,19 @@ class TestGetAttachmentsContent:
result = sp._get_attachments_content(["bad"], "u")
assert result == []
@pytest.mark.unit
def test_none_ids_returns_empty(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(request_data={}, decoded_token={"sub": "u"})
result = sp._get_attachments_content(None, "u")
assert result == []
class TestResolveAgentId:
@@ -250,6 +292,23 @@ class TestResolveAgentId:
sp.conversation_service.get_conversation.side_effect = Exception("db error")
assert sp._resolve_agent_id() is None
@pytest.mark.unit
def test_conversation_without_agent_id(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(
request_data={"conversation_id": "conv1"},
decoded_token={"sub": "u"},
)
sp.conversation_service = MagicMock()
sp.conversation_service.get_conversation.return_value = {"name": "test conv"}
assert sp._resolve_agent_id() is None
class TestGetPromptContent:
@@ -299,6 +358,19 @@ class TestGetPromptContent:
sp.agent_config = {"prompt_id": "bad_id"}
assert sp._get_prompt_content() is None
@pytest.mark.unit
def test_agent_config_not_dict(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(request_data={}, decoded_token={"sub": "u"})
sp.agent_config = "not_a_dict"
assert sp._get_prompt_content() is None
class TestGetRequiredToolActions:
@@ -329,3 +401,114 @@ class TestGetRequiredToolActions:
sp._prompt_content = "No template syntax here"
result = sp._get_required_tool_actions()
assert result == {}
@pytest.mark.unit
def test_caches_result(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(request_data={}, decoded_token={"sub": "u"})
sp._required_tool_actions = {"tool1": {"action1"}}
result = sp._get_required_tool_actions()
assert result == {"tool1": {"action1"}}
class TestConfigureRetriever:
@pytest.mark.unit
def test_default_values(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(
request_data={"question": "Q"},
decoded_token={"sub": "u"},
)
sp.model_id = "test-model"
sp.agent_key = None
sp._configure_retriever()
assert sp.retriever_config["retriever_name"] == "classic"
assert sp.retriever_config["chunks"] == 2
@pytest.mark.unit
def test_isNoneDoc_sets_zero_chunks(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(
request_data={"question": "Q", "isNoneDoc": True},
decoded_token={"sub": "u"},
)
sp.model_id = "test-model"
sp.agent_key = None
sp._configure_retriever()
assert sp.retriever_config["chunks"] == 0
@pytest.mark.unit
def test_custom_retriever_and_chunks(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(
request_data={"question": "Q", "retriever": "hybrid", "chunks": "5"},
decoded_token={"sub": "u"},
)
sp.model_id = "test-model"
sp.agent_key = None
sp._configure_retriever()
assert sp.retriever_config["retriever_name"] == "hybrid"
assert sp.retriever_config["chunks"] == 5
class TestConfigureSource:
@pytest.mark.unit
def test_active_docs_from_request(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(
request_data={"question": "Q", "active_docs": "source_123"},
decoded_token={"sub": "u"},
)
sp.agent_key = None
sp._configure_source()
assert sp.source == {"active_docs": "source_123"}
@pytest.mark.unit
def test_no_source_config(self):
mock_db = MagicMock()
with patch("application.api.answer.services.stream_processor.MongoDB") as MockMongo, \
patch("application.api.answer.services.stream_processor.settings") as mock_settings:
mock_settings.MONGO_DB_NAME = "docsgpt"
MockMongo.get_client.return_value = {"docsgpt": mock_db}
from application.api.answer.services.stream_processor import StreamProcessor
sp = StreamProcessor(
request_data={"question": "Q"},
decoded_token={"sub": "u"},
)
sp.agent_key = None
sp._configure_source()
assert sp.source == {}
assert sp.all_sources == []
+416
View File
@@ -0,0 +1,416 @@
"""Unit tests for application/api/internal/routes.py.
Covers:
- verify_internal_key: key validation
- /api/download: file download
- /api/upload_index: index file upload (existing & new entries)
"""
import io
import json
from unittest.mock import MagicMock
import pytest
from bson.objectid import ObjectId
@pytest.fixture
def internal_app(monkeypatch, mock_mongo_db):
"""Create a Flask app with the internal blueprint registered."""
from flask import Flask
# Patch module-level MongoDB references before importing routes
from application.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
monkeypatch.setattr(
"application.api.internal.routes.conversations_collection",
db["conversations"],
)
monkeypatch.setattr(
"application.api.internal.routes.sources_collection",
db["sources"],
)
from application.api.internal.routes import internal
app = Flask(__name__)
app.register_blueprint(internal)
app.config["TESTING"] = True
return app, db
# ---------------------------------------------------------------------------
# verify_internal_key
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestVerifyInternalKey:
def test_no_internal_key_configured_allows_access(
self, internal_app, monkeypatch
):
app, db = internal_app
monkeypatch.setattr(
"application.api.internal.routes.settings",
MagicMock(
INTERNAL_KEY=None,
UPLOAD_FOLDER="uploads",
VECTOR_STORE="faiss",
EMBEDDINGS_NAME="test",
MONGO_DB_NAME="docsgpt",
),
)
with app.test_client() as client:
# download will fail for missing file but should not be 401
resp = client.get("/api/download?user=u&name=n&file=f")
assert resp.status_code != 401
def test_missing_key_returns_401(self, internal_app, monkeypatch):
app, db = internal_app
monkeypatch.setattr(
"application.api.internal.routes.settings",
MagicMock(
INTERNAL_KEY="secret123",
UPLOAD_FOLDER="uploads",
VECTOR_STORE="faiss",
EMBEDDINGS_NAME="test",
MONGO_DB_NAME="docsgpt",
),
)
with app.test_client() as client:
resp = client.get("/api/download?user=u&name=n&file=f")
assert resp.status_code == 401
def test_wrong_key_returns_401(self, internal_app, monkeypatch):
app, db = internal_app
monkeypatch.setattr(
"application.api.internal.routes.settings",
MagicMock(
INTERNAL_KEY="secret123",
UPLOAD_FOLDER="uploads",
VECTOR_STORE="faiss",
EMBEDDINGS_NAME="test",
MONGO_DB_NAME="docsgpt",
),
)
with app.test_client() as client:
resp = client.get(
"/api/download?user=u&name=n&file=f",
headers={"X-Internal-Key": "wrong"},
)
assert resp.status_code == 401
def test_correct_key_allows_access(self, internal_app, monkeypatch):
app, db = internal_app
monkeypatch.setattr(
"application.api.internal.routes.settings",
MagicMock(
INTERNAL_KEY="secret123",
UPLOAD_FOLDER="uploads",
VECTOR_STORE="faiss",
EMBEDDINGS_NAME="test",
MONGO_DB_NAME="docsgpt",
),
)
with app.test_client() as client:
# Will 404 for missing file, but should pass auth check
resp = client.get(
"/api/download?user=u&name=n&file=f",
headers={"X-Internal-Key": "secret123"},
)
assert resp.status_code != 401
# ---------------------------------------------------------------------------
# /api/upload_index
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestUploadIndex:
def _make_settings(self, vector_store="faiss"):
return MagicMock(
INTERNAL_KEY=None,
UPLOAD_FOLDER="uploads",
VECTOR_STORE=vector_store,
EMBEDDINGS_NAME="test_embeddings",
MONGO_DB_NAME="docsgpt",
)
def test_missing_user_returns_no_user(self, internal_app, monkeypatch):
app, db = internal_app
monkeypatch.setattr(
"application.api.internal.routes.settings", self._make_settings()
)
with app.test_client() as client:
resp = client.post("/api/upload_index", data={})
assert resp.json["status"] == "no user"
def test_missing_name_returns_no_name(self, internal_app, monkeypatch):
app, db = internal_app
monkeypatch.setattr(
"application.api.internal.routes.settings", self._make_settings()
)
with app.test_client() as client:
resp = client.post("/api/upload_index", data={"user": "testuser"})
assert resp.json["status"] == "no name"
def test_creates_new_source_entry(self, internal_app, monkeypatch):
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": "testuser",
"name": "testjob",
"tokens": "100",
"retriever": "classic",
"id": doc_id,
"type": "local",
},
)
assert resp.json["status"] == "ok"
entry = db["sources"].find_one({"_id": ObjectId(doc_id)})
assert entry is not None
assert entry["user"] == "testuser"
assert entry["name"] == "testjob"
def test_updates_existing_source_entry(self, internal_app, monkeypatch):
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)),
)
# Insert existing entry
db["sources"].insert_one(
{"_id": doc_id, "user": "old_user", "name": "old_name"}
)
with app.test_client() as client:
resp = client.post(
"/api/upload_index",
data={
"user": "new_user",
"name": "new_name",
"tokens": "200",
"retriever": "hybrid",
"id": str(doc_id),
"type": "remote",
},
)
assert resp.json["status"] == "ok"
entry = db["sources"].find_one({"_id": doc_id})
assert entry["user"] == "new_user"
assert entry["name"] == "new_name"
def test_parses_directory_structure_json(self, internal_app, monkeypatch):
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)),
)
dir_struct = {"root": {"files": ["a.txt", "b.txt"]}}
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",
"directory_structure": json.dumps(dir_struct),
},
)
assert resp.json["status"] == "ok"
entry = db["sources"].find_one({"_id": ObjectId(doc_id)})
assert entry["directory_structure"] == dir_struct
def test_invalid_directory_structure_defaults_empty(
self, internal_app, monkeypatch
):
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",
"directory_structure": "not valid json",
},
)
assert resp.json["status"] == "ok"
entry = db["sources"].find_one({"_id": ObjectId(doc_id)})
assert entry["directory_structure"] == {}
def test_file_name_map_parsed(self, internal_app, monkeypatch):
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)),
)
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": doc_id,
"type": "local",
"file_name_map": json.dumps(fmap),
},
)
assert resp.json["status"] == "ok"
entry = db["sources"].find_one({"_id": ObjectId(doc_id)})
assert entry["file_name_map"] == fmap
def test_faiss_missing_files_returns_no_file(
self, internal_app, monkeypatch
):
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",
},
)
assert resp.json["status"] == "no file"
def test_faiss_empty_filename_returns_no_file_name(
self, internal_app, monkeypatch
):
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""), ""),
},
)
assert resp.json["status"] == "no file name"
def test_remote_data_and_sync_frequency(self, internal_app, monkeypatch):
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": "remote",
"remote_data": '{"url":"http://example.com"}',
"sync_frequency": "daily",
},
)
assert resp.json["status"] == "ok"
entry = db["sources"].find_one({"_id": ObjectId(doc_id)})
assert entry["sync_frequency"] == "daily"
assert entry["remote_data"] == '{"url":"http://example.com"}'
View File
Whitespace-only changes.
+879
View File
@@ -0,0 +1,879 @@
"""Tests for source chunk management routes."""
import pytest
from unittest.mock import Mock, patch
from bson import ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
def _status(response):
if isinstance(response, tuple):
return response[1]
return response.status_code
def _json(response):
if isinstance(response, tuple):
return response[0].json
return response.json
# ---------------------------------------------------------------------------
# GetChunks
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestGetChunks:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.chunks import GetChunks
with app.test_request_context("/api/get_chunks?id=abc"):
from flask import request
request.decoded_token = None
response = GetChunks().get()
assert _status(response) == 401
def test_returns_400_for_invalid_doc_id(self, app):
from application.api.user.sources.chunks import GetChunks
with app.test_request_context("/api/get_chunks?id=invalid"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetChunks().get()
assert _status(response) == 400
assert "Invalid doc_id" in _json(response)["error"]
def test_returns_404_when_doc_not_found(self, app):
from application.api.user.sources.chunks import GetChunks
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
):
with app.test_request_context(f"/api/get_chunks?id={doc_id}"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetChunks().get()
assert _status(response) == 404
assert "not found" in _json(response)["error"]
def test_returns_paginated_chunks(self, app):
from application.api.user.sources.chunks import GetChunks
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
chunks = [
{"text": f"chunk {i}", "metadata": {}, "doc_id": f"c{i}"}
for i in range(25)
]
mock_store = Mock()
mock_store.get_chunks.return_value = chunks
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
f"/api/get_chunks?id={doc_id}&page=2&per_page=10"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = GetChunks().get()
assert _status(response) == 200
data = _json(response)
assert data["total"] == 25
assert data["page"] == 2
assert data["per_page"] == 10
assert len(data["chunks"]) == 10
def test_filters_chunks_by_path(self, app):
from application.api.user.sources.chunks import GetChunks
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
chunks = [
{"text": "a", "metadata": {"source": "inputs/dir/file.pdf"}, "doc_id": "c1"},
{"text": "b", "metadata": {"source": "inputs/other.txt"}, "doc_id": "c2"},
{"text": "c", "metadata": {"file_path": "guides/setup.md"}, "doc_id": "c3"},
]
mock_store = Mock()
mock_store.get_chunks.return_value = chunks
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
f"/api/get_chunks?id={doc_id}&path=file.pdf"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = GetChunks().get()
data = _json(response)
assert data["total"] == 1
assert data["chunks"][0]["text"] == "a"
assert data["path"] == "file.pdf"
def test_filters_chunks_by_file_path_metadata(self, app):
from application.api.user.sources.chunks import GetChunks
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
chunks = [
{"text": "a", "metadata": {"source": "inputs/dir/file.pdf"}, "doc_id": "c1"},
{"text": "c", "metadata": {"file_path": "guides/setup.md"}, "doc_id": "c3"},
]
mock_store = Mock()
mock_store.get_chunks.return_value = chunks
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
f"/api/get_chunks?id={doc_id}&path=setup.md"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = GetChunks().get()
data = _json(response)
assert data["total"] == 1
assert data["chunks"][0]["text"] == "c"
def test_filters_chunks_by_search_term(self, app):
from application.api.user.sources.chunks import GetChunks
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
chunks = [
{"text": "Python is great", "metadata": {"title": "intro"}, "doc_id": "c1"},
{"text": "Java tutorial", "metadata": {"title": "java guide"}, "doc_id": "c2"},
{"text": "Hello world", "metadata": {"title": "Python Basics"}, "doc_id": "c3"},
]
mock_store = Mock()
mock_store.get_chunks.return_value = chunks
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
f"/api/get_chunks?id={doc_id}&search=python"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = GetChunks().get()
data = _json(response)
assert data["total"] == 2
assert data["search"] == "python"
def test_combines_path_and_search_filters(self, app):
from application.api.user.sources.chunks import GetChunks
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
chunks = [
{"text": "Python intro", "metadata": {"source": "dir/intro.md", "title": ""}, "doc_id": "c1"},
{"text": "Python deep", "metadata": {"source": "dir/deep.md", "title": ""}, "doc_id": "c2"},
{"text": "Java intro", "metadata": {"source": "dir/intro.md", "title": ""}, "doc_id": "c3"},
]
mock_store = Mock()
mock_store.get_chunks.return_value = chunks
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
f"/api/get_chunks?id={doc_id}&path=intro.md&search=python"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = GetChunks().get()
data = _json(response)
assert data["total"] == 1
assert data["chunks"][0]["doc_id"] == "c1"
def test_returns_500_on_store_error(self, app):
from application.api.user.sources.chunks import GetChunks
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.get_chunks.side_effect = Exception("Store error")
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(f"/api/get_chunks?id={doc_id}"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = GetChunks().get()
assert _status(response) == 500
def test_no_path_or_search_returns_null_fields(self, app):
from application.api.user.sources.chunks import GetChunks
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.get_chunks.return_value = []
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(f"/api/get_chunks?id={doc_id}"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = GetChunks().get()
data = _json(response)
assert data["path"] is None
assert data["search"] is None
# ---------------------------------------------------------------------------
# AddChunk
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestAddChunk:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.chunks import AddChunk
with app.test_request_context(
"/api/add_chunk", method="POST", json={"id": "abc", "text": "hi"}
):
from flask import request
request.decoded_token = None
response = AddChunk().post()
assert _status(response) == 401
def test_returns_400_missing_required_fields(self, app):
from application.api.user.sources.chunks import AddChunk
with app.test_request_context(
"/api/add_chunk", method="POST", json={"id": str(ObjectId())}
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = AddChunk().post()
# check_required_fields returns a tuple (response, status)
assert response is not None
def test_returns_400_for_invalid_doc_id(self, app):
from application.api.user.sources.chunks import AddChunk
with app.test_request_context(
"/api/add_chunk", method="POST", json={"id": "bad", "text": "hi"}
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = AddChunk().post()
assert _status(response) == 400
assert "Invalid doc_id" in _json(response)["error"]
def test_returns_404_when_doc_not_found(self, app):
from application.api.user.sources.chunks import AddChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/add_chunk", method="POST",
json={"id": doc_id, "text": "hello"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = AddChunk().post()
assert _status(response) == 404
def test_adds_chunk_successfully(self, app):
from application.api.user.sources.chunks import AddChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.add_chunk.return_value = "new-chunk-id"
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
), patch(
"application.api.user.sources.chunks.num_tokens_from_string",
return_value=5,
):
with app.test_request_context(
"/api/add_chunk", method="POST",
json={"id": doc_id, "text": "hello world"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = AddChunk().post()
assert _status(response) == 201
data = _json(response)
assert data["chunk_id"] == "new-chunk-id"
assert "successfully" in data["message"]
call_args = mock_store.add_chunk.call_args
assert call_args[0][0] == "hello world"
assert call_args[0][1]["token_count"] == 5
def test_adds_chunk_with_custom_metadata(self, app):
from application.api.user.sources.chunks import AddChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.add_chunk.return_value = "cid"
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
), patch(
"application.api.user.sources.chunks.num_tokens_from_string",
return_value=3,
):
with app.test_request_context(
"/api/add_chunk", method="POST",
json={
"id": doc_id,
"text": "hi",
"metadata": {"source": "test.pdf"},
},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = AddChunk().post()
assert _status(response) == 201
meta = mock_store.add_chunk.call_args[0][1]
assert meta["source"] == "test.pdf"
assert meta["token_count"] == 3
def test_returns_500_on_store_error(self, app):
from application.api.user.sources.chunks import AddChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.add_chunk.side_effect = Exception("fail")
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
), patch(
"application.api.user.sources.chunks.num_tokens_from_string",
return_value=1,
):
with app.test_request_context(
"/api/add_chunk", method="POST",
json={"id": doc_id, "text": "hello"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = AddChunk().post()
assert _status(response) == 500
# ---------------------------------------------------------------------------
# DeleteChunk
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestDeleteChunk:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.chunks import DeleteChunk
with app.test_request_context("/api/delete_chunk?id=abc&chunk_id=xyz"):
from flask import request
request.decoded_token = None
response = DeleteChunk().delete()
assert _status(response) == 401
def test_returns_400_for_invalid_doc_id(self, app):
from application.api.user.sources.chunks import DeleteChunk
with app.test_request_context("/api/delete_chunk?id=bad&chunk_id=xyz"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteChunk().delete()
assert _status(response) == 400
assert "Invalid doc_id" in _json(response)["error"]
def test_returns_404_when_doc_not_found(self, app):
from application.api.user.sources.chunks import DeleteChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
):
with app.test_request_context(
f"/api/delete_chunk?id={doc_id}&chunk_id=cid"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteChunk().delete()
assert _status(response) == 404
def test_deletes_chunk_successfully(self, app):
from application.api.user.sources.chunks import DeleteChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.delete_chunk.return_value = True
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
f"/api/delete_chunk?id={doc_id}&chunk_id=cid"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteChunk().delete()
assert _status(response) == 200
assert "successfully" in _json(response)["message"]
mock_store.delete_chunk.assert_called_once_with("cid")
def test_returns_404_when_chunk_not_deleted(self, app):
from application.api.user.sources.chunks import DeleteChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.delete_chunk.return_value = False
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
f"/api/delete_chunk?id={doc_id}&chunk_id=missing"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteChunk().delete()
assert _status(response) == 404
assert "not found" in _json(response)["message"]
def test_returns_500_on_store_error(self, app):
from application.api.user.sources.chunks import DeleteChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.delete_chunk.side_effect = Exception("boom")
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
f"/api/delete_chunk?id={doc_id}&chunk_id=cid"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteChunk().delete()
assert _status(response) == 500
# ---------------------------------------------------------------------------
# UpdateChunk
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestUpdateChunk:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.chunks import UpdateChunk
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={"id": "abc", "chunk_id": "cid"},
):
from flask import request
request.decoded_token = None
response = UpdateChunk().put()
assert _status(response) == 401
def test_returns_400_missing_required_fields(self, app):
from application.api.user.sources.chunks import UpdateChunk
with app.test_request_context(
"/api/update_chunk", method="PUT", json={"id": str(ObjectId())}
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
assert response is not None
def test_returns_400_for_invalid_doc_id(self, app):
from application.api.user.sources.chunks import UpdateChunk
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={"id": "bad", "chunk_id": "cid"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
assert _status(response) == 400
def test_returns_404_when_doc_not_found(self, app):
from application.api.user.sources.chunks import UpdateChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={"id": doc_id, "chunk_id": "cid"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
assert _status(response) == 404
def test_returns_404_when_chunk_not_found(self, app):
from application.api.user.sources.chunks import UpdateChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.get_chunks.return_value = [
{"doc_id": "other", "text": "x", "metadata": {}},
]
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={"id": doc_id, "chunk_id": "missing"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
assert _status(response) == 404
assert "Chunk not found" in _json(response)["error"]
def test_updates_chunk_text_successfully(self, app):
from application.api.user.sources.chunks import UpdateChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.get_chunks.return_value = [
{"doc_id": "cid", "text": "old text", "metadata": {"source": "f.pdf"}},
]
mock_store.add_chunk.return_value = "new-cid"
mock_store.delete_chunk.return_value = True
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
), patch(
"application.api.user.sources.chunks.num_tokens_from_string",
return_value=7,
):
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={"id": doc_id, "chunk_id": "cid", "text": "new text"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
assert _status(response) == 200
data = _json(response)
assert data["chunk_id"] == "new-cid"
assert data["original_chunk_id"] == "cid"
# Verify add was called with new text and merged metadata
add_call = mock_store.add_chunk.call_args
assert add_call[0][0] == "new text"
assert add_call[0][1]["source"] == "f.pdf"
assert add_call[0][1]["token_count"] == 7
mock_store.delete_chunk.assert_called_once_with("cid")
def test_updates_chunk_metadata_only(self, app):
from application.api.user.sources.chunks import UpdateChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.get_chunks.return_value = [
{"doc_id": "cid", "text": "keep me", "metadata": {"source": "f.pdf"}},
]
mock_store.add_chunk.return_value = "new-cid"
mock_store.delete_chunk.return_value = True
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={
"id": doc_id,
"chunk_id": "cid",
"metadata": {"title": "new title"},
},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
assert _status(response) == 200
add_call = mock_store.add_chunk.call_args
# text should be preserved
assert add_call[0][0] == "keep me"
# metadata should be merged
assert add_call[0][1]["source"] == "f.pdf"
assert add_call[0][1]["title"] == "new title"
def test_update_warns_when_old_chunk_delete_fails(self, app):
from application.api.user.sources.chunks import UpdateChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.get_chunks.return_value = [
{"doc_id": "cid", "text": "text", "metadata": {}},
]
mock_store.add_chunk.return_value = "new-cid"
mock_store.delete_chunk.return_value = False # delete fails
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={"id": doc_id, "chunk_id": "cid"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
# Still returns 200 with a warning logged
assert _status(response) == 200
def test_returns_500_when_add_chunk_fails(self, app):
from application.api.user.sources.chunks import UpdateChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.get_chunks.return_value = [
{"doc_id": "cid", "text": "text", "metadata": {}},
]
mock_store.add_chunk.side_effect = Exception("add failed")
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={"id": doc_id, "chunk_id": "cid", "text": "new"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
assert _status(response) == 500
assert "addition failed" in _json(response)["error"]
def test_returns_500_on_general_store_error(self, app):
from application.api.user.sources.chunks import UpdateChunk
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = {"_id": ObjectId(doc_id), "user": "u1"}
mock_store = Mock()
mock_store.get_chunks.side_effect = Exception("connection lost")
with patch(
"application.api.user.sources.chunks.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.chunks.get_vector_store",
return_value=mock_store,
):
with app.test_request_context(
"/api/update_chunk", method="PUT",
json={"id": doc_id, "chunk_id": "cid"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = UpdateChunk().put()
assert _status(response) == 500
@@ -0,0 +1,965 @@
"""Tests for source management routes (CombinedJson, PaginatedSources,
DeleteByIds, DeleteOldIndexes, ManageSync, DirectoryStructure).
Note: SyncSource and _get_provider_from_remote_data are already covered in
test_routes.py and are NOT duplicated here.
"""
import json
import pytest
from unittest.mock import MagicMock, Mock, patch
from bson import ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
def _status(response):
if isinstance(response, tuple):
return response[1]
return response.status_code
def _json(response):
if isinstance(response, tuple):
return response[0].json
return response.json
# ---------------------------------------------------------------------------
# CombinedJson (/api/sources)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestCombinedJson:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import CombinedJson
with app.test_request_context("/api/sources"):
from flask import request
request.decoded_token = None
response = CombinedJson().get()
assert _status(response) == 401
def test_returns_default_source_plus_user_sources(self, app):
from application.api.user.sources.routes import CombinedJson
src_id = ObjectId()
mock_cursor = MagicMock()
mock_cursor.sort.return_value = [
{
"_id": src_id,
"name": "My Doc",
"date": "2024-01-01",
"tokens": "100",
"retriever": "classic",
"sync_frequency": "daily",
"remote_data": json.dumps({"provider": "github"}),
"directory_structure": None,
"type": "file",
}
]
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/sources"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = CombinedJson().get()
assert _status(response) == 200
data = _json(response)
# First entry is always the Default
assert data[0]["name"] == "Default"
assert data[0]["date"] == "default"
# Second entry is user source
assert data[1]["id"] == str(src_id)
assert data[1]["name"] == "My Doc"
assert data[1]["provider"] == "github"
assert data[1]["syncFrequency"] == "daily"
assert data[1]["is_nested"] is False
def test_is_nested_true_when_directory_structure_present(self, app):
from application.api.user.sources.routes import CombinedJson
mock_cursor = MagicMock()
mock_cursor.sort.return_value = [
{
"_id": ObjectId(),
"name": "Nested",
"date": "2024-01-01",
"directory_structure": {"files": ["a.txt"]},
}
]
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/sources"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = CombinedJson().get()
data = _json(response)
assert data[1]["is_nested"] is True
def test_returns_400_on_db_error(self, app):
from application.api.user.sources.routes import CombinedJson
mock_collection = Mock()
mock_collection.find.side_effect = Exception("db err")
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/sources"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = CombinedJson().get()
assert _status(response) == 400
def test_type_defaults_to_file(self, app):
from application.api.user.sources.routes import CombinedJson
mock_cursor = MagicMock()
mock_cursor.sort.return_value = [
{"_id": ObjectId(), "name": "X", "date": "d"}
]
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/sources"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = CombinedJson().get()
data = _json(response)
assert data[1]["type"] == "file"
# ---------------------------------------------------------------------------
# PaginatedSources (/api/sources/paginated)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestPaginatedSources:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import PaginatedSources
with app.test_request_context("/api/sources/paginated"):
from flask import request
request.decoded_token = None
response = PaginatedSources().get()
assert _status(response) == 401
def test_returns_paginated_results(self, app):
from application.api.user.sources.routes import PaginatedSources
ids = [ObjectId() for _ in range(3)]
docs = [
{"_id": ids[i], "name": f"Doc{i}", "date": f"2024-0{i + 1}-01"}
for i in range(3)
]
mock_cursor = MagicMock()
mock_cursor.sort.return_value = mock_cursor
mock_cursor.skip.return_value = mock_cursor
mock_cursor.limit.return_value = docs
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
mock_collection.count_documents.return_value = 3
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/sources/paginated?page=1&rows=10&sort=date&order=desc"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = PaginatedSources().get()
assert _status(response) == 200
data = _json(response)
assert data["total"] == 3
assert data["totalPages"] == 1
assert data["currentPage"] == 1
assert len(data["paginated"]) == 3
def test_search_filter_applies_regex(self, app):
from application.api.user.sources.routes import PaginatedSources
mock_cursor = MagicMock()
mock_cursor.sort.return_value = mock_cursor
mock_cursor.skip.return_value = mock_cursor
mock_cursor.limit.return_value = []
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
mock_collection.count_documents.return_value = 0
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/sources/paginated?search=test%20doc"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = PaginatedSources().get()
assert _status(response) == 200
# Verify search query was passed
query_arg = mock_collection.count_documents.call_args[0][0]
assert query_arg["name"]["$regex"] == "test doc"
assert query_arg["name"]["$options"] == "i"
def test_ascending_sort_order(self, app):
from application.api.user.sources.routes import PaginatedSources
mock_cursor = MagicMock()
mock_cursor.sort.return_value = mock_cursor
mock_cursor.skip.return_value = mock_cursor
mock_cursor.limit.return_value = []
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
mock_collection.count_documents.return_value = 0
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/sources/paginated?order=asc&sort=name"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = PaginatedSources().get()
assert _status(response) == 200
mock_cursor.sort.assert_called_once_with("name", 1)
def test_page_clamped_to_valid_range(self, app):
from application.api.user.sources.routes import PaginatedSources
mock_cursor = MagicMock()
mock_cursor.sort.return_value = mock_cursor
mock_cursor.skip.return_value = mock_cursor
mock_cursor.limit.return_value = []
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
mock_collection.count_documents.return_value = 5 # 1 page with default 10 rows
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/sources/paginated?page=999"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = PaginatedSources().get()
data = _json(response)
assert data["currentPage"] == 1 # clamped
def test_returns_400_on_db_error(self, app):
from application.api.user.sources.routes import PaginatedSources
mock_collection = Mock()
mock_collection.count_documents.return_value = 0
mock_collection.find.side_effect = Exception("db error")
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/sources/paginated"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = PaginatedSources().get()
assert _status(response) == 400
def test_paginated_includes_provider_and_is_nested(self, app):
from application.api.user.sources.routes import PaginatedSources
doc = {
"_id": ObjectId(),
"name": "S3 Src",
"date": "2024-01-01",
"remote_data": {"provider": "s3"},
"directory_structure": {"dirs": ["a"]},
"type": "s3",
}
mock_cursor = MagicMock()
mock_cursor.sort.return_value = mock_cursor
mock_cursor.skip.return_value = mock_cursor
mock_cursor.limit.return_value = [doc]
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
mock_collection.count_documents.return_value = 1
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/sources/paginated"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = PaginatedSources().get()
data = _json(response)
entry = data["paginated"][0]
assert entry["provider"] == "s3"
assert entry["isNested"] is True
assert entry["type"] == "s3"
# ---------------------------------------------------------------------------
# DeleteByIds (/api/delete_by_ids)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestDeleteByIds:
def test_returns_400_when_path_missing(self, app):
from application.api.user.sources.routes import DeleteByIds
with app.test_request_context("/api/delete_by_ids"):
response = DeleteByIds().get()
assert _status(response) == 400
assert "Missing" in _json(response)["message"]
def test_returns_200_on_successful_delete(self, app):
from application.api.user.sources.routes import DeleteByIds
mock_collection = Mock()
mock_collection.delete_index.return_value = True
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/delete_by_ids?path=id1,id2"):
response = DeleteByIds().get()
assert _status(response) == 200
assert _json(response)["success"] is True
mock_collection.delete_index.assert_called_once_with(ids="id1,id2")
def test_returns_400_when_delete_returns_false(self, app):
from application.api.user.sources.routes import DeleteByIds
mock_collection = Mock()
mock_collection.delete_index.return_value = False
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/delete_by_ids?path=id1"):
response = DeleteByIds().get()
assert _status(response) == 400
def test_returns_400_on_exception(self, app):
from application.api.user.sources.routes import DeleteByIds
mock_collection = Mock()
mock_collection.delete_index.side_effect = Exception("fail")
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context("/api/delete_by_ids?path=id1"):
response = DeleteByIds().get()
assert _status(response) == 400
# ---------------------------------------------------------------------------
# DeleteOldIndexes (/api/delete_old)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestDeleteOldIndexes:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
with app.test_request_context("/api/delete_old?source_id=abc"):
from flask import request
request.decoded_token = None
response = DeleteOldIndexes().get()
assert _status(response) == 401
def test_returns_400_when_source_id_missing(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
with app.test_request_context("/api/delete_old"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteOldIndexes().get()
assert _status(response) == 400
def test_returns_404_when_doc_not_found(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
source_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(f"/api/delete_old?source_id={source_id}"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteOldIndexes().get()
assert _status(response) == 404
def test_deletes_faiss_index_and_file(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
source_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": source_id,
"user": "u1",
"file_path": "uploads/u1/doc.pdf",
}
mock_storage = Mock()
mock_storage.file_exists.return_value = True
mock_storage.is_directory.return_value = False
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.routes.StorageCreator.get_storage",
return_value=mock_storage,
), patch(
"application.api.user.sources.routes.settings"
) as mock_settings:
mock_settings.VECTOR_STORE = "faiss"
with app.test_request_context(
f"/api/delete_old?source_id={source_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteOldIndexes().get()
assert _status(response) == 200
assert _json(response)["success"] is True
# Should have checked and deleted faiss files
assert mock_storage.delete_file.call_count >= 1
mock_collection.delete_one.assert_called_once()
def test_deletes_non_faiss_vector_index(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
source_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": source_id,
"user": "u1",
}
mock_storage = Mock()
mock_vectorstore = Mock()
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.routes.StorageCreator.get_storage",
return_value=mock_storage,
), patch(
"application.api.user.sources.routes.VectorCreator.create_vectorstore",
return_value=mock_vectorstore,
), patch(
"application.api.user.sources.routes.settings"
) as mock_settings:
mock_settings.VECTOR_STORE = "elasticsearch"
with app.test_request_context(
f"/api/delete_old?source_id={source_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteOldIndexes().get()
assert _status(response) == 200
mock_vectorstore.delete_index.assert_called_once()
def test_deletes_directory_of_files(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
source_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": source_id,
"user": "u1",
"file_path": "uploads/u1/mydir",
}
mock_storage = Mock()
mock_storage.is_directory.return_value = True
mock_storage.list_files.return_value = ["uploads/u1/mydir/a.txt", "uploads/u1/mydir/b.txt"]
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.routes.StorageCreator.get_storage",
return_value=mock_storage,
), patch(
"application.api.user.sources.routes.settings"
) as mock_settings:
mock_settings.VECTOR_STORE = "faiss"
mock_storage.file_exists.return_value = False
with app.test_request_context(
f"/api/delete_old?source_id={source_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteOldIndexes().get()
assert _status(response) == 200
# Each file in directory should be deleted
assert mock_storage.delete_file.call_count == 2
def test_handles_file_not_found_gracefully(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
source_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": source_id,
"user": "u1",
"file_path": "uploads/missing.pdf",
}
mock_storage = Mock()
mock_storage.is_directory.side_effect = FileNotFoundError()
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.routes.StorageCreator.get_storage",
return_value=mock_storage,
), patch(
"application.api.user.sources.routes.settings"
) as mock_settings:
mock_settings.VECTOR_STORE = "faiss"
mock_storage.file_exists.return_value = False
with app.test_request_context(
f"/api/delete_old?source_id={source_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteOldIndexes().get()
assert _status(response) == 200
mock_collection.delete_one.assert_called_once()
def test_returns_400_on_general_error(self, app):
from application.api.user.sources.routes import DeleteOldIndexes
source_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": source_id,
"user": "u1",
}
mock_storage = Mock()
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
), patch(
"application.api.user.sources.routes.StorageCreator.get_storage",
return_value=mock_storage,
), patch(
"application.api.user.sources.routes.settings"
) as mock_settings:
mock_settings.VECTOR_STORE = "faiss"
mock_storage.file_exists.side_effect = RuntimeError("disk error")
with app.test_request_context(
f"/api/delete_old?source_id={source_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DeleteOldIndexes().get()
assert _status(response) == 400
# ---------------------------------------------------------------------------
# ManageSync (/api/manage_sync)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestManageSync:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import ManageSync
with app.test_request_context(
"/api/manage_sync", method="POST",
json={"source_id": "x", "sync_frequency": "daily"},
):
from flask import request
request.decoded_token = None
response = ManageSync().post()
assert _status(response) == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.sources.routes import ManageSync
with app.test_request_context(
"/api/manage_sync", method="POST",
json={"source_id": "abc"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = ManageSync().post()
assert response is not None
def test_returns_400_for_invalid_frequency(self, app):
from application.api.user.sources.routes import ManageSync
with app.test_request_context(
"/api/manage_sync", method="POST",
json={"source_id": str(ObjectId()), "sync_frequency": "hourly"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = ManageSync().post()
assert _status(response) == 400
assert "Invalid frequency" in _json(response)["message"]
def test_updates_sync_frequency_successfully(self, app):
from application.api.user.sources.routes import ManageSync
source_id = str(ObjectId())
mock_collection = Mock()
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/manage_sync", method="POST",
json={"source_id": source_id, "sync_frequency": "weekly"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = ManageSync().post()
assert _status(response) == 200
assert _json(response)["success"] is True
call_args = mock_collection.update_one.call_args
assert call_args[0][0]["_id"] == ObjectId(source_id)
assert call_args[0][0]["user"] == "u1"
assert call_args[0][1]["$set"]["sync_frequency"] == "weekly"
def test_accepts_all_valid_frequencies(self, app):
from application.api.user.sources.routes import ManageSync
mock_collection = Mock()
for freq in ["never", "daily", "weekly", "monthly"]:
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/manage_sync", method="POST",
json={"source_id": str(ObjectId()), "sync_frequency": freq},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = ManageSync().post()
assert _status(response) == 200
def test_returns_400_on_db_error(self, app):
from application.api.user.sources.routes import ManageSync
mock_collection = Mock()
mock_collection.update_one.side_effect = Exception("db err")
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
"/api/manage_sync", method="POST",
json={"source_id": str(ObjectId()), "sync_frequency": "daily"},
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = ManageSync().post()
assert _status(response) == 400
# ---------------------------------------------------------------------------
# RedirectToSources (/api/combine)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestRedirectToSources:
def test_redirects_to_sources(self, app):
from application.api.user.sources.routes import RedirectToSources
with app.test_request_context("/api/combine"):
response = RedirectToSources().get()
assert response.status_code == 301
assert response.location == "/api/sources"
# ---------------------------------------------------------------------------
# DirectoryStructure (/api/directory_structure)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestDirectoryStructure:
def test_returns_401_unauthenticated(self, app):
from application.api.user.sources.routes import DirectoryStructure
with app.test_request_context("/api/directory_structure?id=abc"):
from flask import request
request.decoded_token = None
response = DirectoryStructure().get()
assert _status(response) == 401
def test_returns_400_when_id_missing(self, app):
from application.api.user.sources.routes import DirectoryStructure
with app.test_request_context("/api/directory_structure"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DirectoryStructure().get()
assert _status(response) == 400
assert "required" in _json(response)["error"]
def test_returns_400_for_invalid_doc_id(self, app):
from application.api.user.sources.routes import DirectoryStructure
with app.test_request_context("/api/directory_structure?id=invalid"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DirectoryStructure().get()
assert _status(response) == 400
assert "Invalid" in _json(response)["error"]
def test_returns_404_when_doc_not_found(self, app):
from application.api.user.sources.routes import DirectoryStructure
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(f"/api/directory_structure?id={doc_id}"):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DirectoryStructure().get()
assert _status(response) == 404
assert "not found" in _json(response)["error"]
def test_returns_directory_structure(self, app):
from application.api.user.sources.routes import DirectoryStructure
doc_id = ObjectId()
dir_struct = {"dirs": ["a", "b"], "files": ["c.txt"]}
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": doc_id,
"user": "u1",
"directory_structure": dir_struct,
"file_path": "uploads/u1/mydir",
"remote_data": json.dumps({"provider": "github"}),
}
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
f"/api/directory_structure?id={doc_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DirectoryStructure().get()
assert _status(response) == 200
data = _json(response)
assert data["success"] is True
assert data["directory_structure"] == dir_struct
assert data["base_path"] == "uploads/u1/mydir"
assert data["provider"] == "github"
def test_returns_none_provider_when_no_remote_data(self, app):
from application.api.user.sources.routes import DirectoryStructure
doc_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": doc_id,
"user": "u1",
"directory_structure": {},
"file_path": "path",
}
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
f"/api/directory_structure?id={doc_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DirectoryStructure().get()
data = _json(response)
assert data["provider"] is None
def test_handles_invalid_remote_data_json(self, app):
from application.api.user.sources.routes import DirectoryStructure
doc_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": doc_id,
"user": "u1",
"directory_structure": {},
"file_path": "path",
"remote_data": "not-valid-json{",
}
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
f"/api/directory_structure?id={doc_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DirectoryStructure().get()
data = _json(response)
assert data["success"] is True
assert data["provider"] is None
def test_returns_500_on_general_error(self, app):
from application.api.user.sources.routes import DirectoryStructure
doc_id = str(ObjectId())
mock_collection = Mock()
mock_collection.find_one.side_effect = Exception("db error")
with patch(
"application.api.user.sources.routes.sources_collection",
mock_collection,
):
with app.test_request_context(
f"/api/directory_structure?id={doc_id}"
):
from flask import request
request.decoded_token = {"sub": "u1"}
response = DirectoryStructure().get()
assert _status(response) == 500
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+768
View File
@@ -0,0 +1,768 @@
"""Tests for application.api.user.agents.sharing module."""
from unittest.mock import Mock, patch
import pytest
from bson import DBRef, ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
# ---------------------------------------------------------------------------
# SharedAgent (GET /shared_agent)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestSharedAgent:
def test_returns_400_missing_token(self, app):
from application.api.user.agents.sharing import SharedAgent
with app.test_request_context("/api/shared_agent"):
response = SharedAgent().get()
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app):
from application.api.user.agents.sharing import SharedAgent
mock_col = Mock()
mock_col.find_one.return_value = None
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context("/api/shared_agent?token=abc123"):
response = SharedAgent().get()
assert response.status_code == 404
def test_returns_shared_agent_data(self, app):
from application.api.user.agents.sharing import SharedAgent
agent_id = ObjectId()
mock_agents_col = Mock()
mock_agents_col.find_one.return_value = {
"_id": agent_id,
"user": "owner1",
"name": "Shared Agent",
"description": "A shared agent",
"chunks": "5",
"retriever": "classic",
"prompt_id": "default",
"tools": [],
"agent_type": "classic",
"status": "published",
"shared_publicly": True,
"shared_token": "abc123",
}
mock_resolve = Mock(return_value=[])
mock_db = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
):
with app.test_request_context("/api/shared_agent?token=abc123"):
from flask import request
# No decoded_token -> anonymous access
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 200
data = response.json
assert data["id"] == str(agent_id)
assert data["name"] == "Shared Agent"
assert data["shared"] is True
def test_adds_to_shared_with_me_for_different_user(self, app):
from application.api.user.agents.sharing import SharedAgent
agent_id = ObjectId()
mock_agents_col = Mock()
mock_agents_col.find_one.return_value = {
"_id": agent_id,
"user": "owner1",
"name": "Agent",
"tools": [],
"shared_publicly": True,
"shared_token": "abc123",
}
mock_resolve = Mock(return_value=[])
mock_db = Mock()
mock_ensure = Mock(return_value={"user_id": "user2"})
mock_users_col = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
), patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.users_collection", mock_users_col
):
with app.test_request_context("/api/shared_agent?token=abc123"):
from flask import request
request.decoded_token = {"sub": "user2"}
response = SharedAgent().get()
assert response.status_code == 200
mock_ensure.assert_called_once_with("user2")
mock_users_col.update_one.assert_called_once()
def test_does_not_add_to_shared_for_owner(self, app):
from application.api.user.agents.sharing import SharedAgent
agent_id = ObjectId()
mock_agents_col = Mock()
mock_agents_col.find_one.return_value = {
"_id": agent_id,
"user": "owner1",
"name": "Agent",
"tools": [],
"shared_publicly": True,
"shared_token": "abc123",
}
mock_resolve = Mock(return_value=[])
mock_db = Mock()
mock_ensure = Mock()
mock_users_col = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
), patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.users_collection", mock_users_col
):
with app.test_request_context("/api/shared_agent?token=abc123"):
from flask import request
request.decoded_token = {"sub": "owner1"}
response = SharedAgent().get()
assert response.status_code == 200
mock_ensure.assert_not_called()
mock_users_col.update_one.assert_not_called()
def test_enriches_tool_names(self, app):
from application.api.user.agents.sharing import SharedAgent
agent_id = ObjectId()
tool_id = str(ObjectId())
mock_agents_col = Mock()
mock_agents_col.find_one.return_value = {
"_id": agent_id,
"user": "owner1",
"name": "Agent",
"tools": [tool_id],
"shared_publicly": True,
"shared_token": "tok",
}
mock_tools_col = Mock()
mock_tools_col.find_one.return_value = {
"_id": ObjectId(tool_id),
"name": "calculator",
}
mock_resolve = Mock(return_value=[])
mock_db = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.user_tools_collection", mock_tools_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
):
with app.test_request_context("/api/shared_agent?token=tok"):
from flask import request
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 200
assert response.json["tools"] == ["calculator"]
def test_handles_source_dbref(self, app):
from application.api.user.agents.sharing import SharedAgent
agent_id = ObjectId()
source_id = ObjectId()
source_ref = DBRef("sources", source_id)
mock_agents_col = Mock()
mock_agents_col.find_one.return_value = {
"_id": agent_id,
"user": "owner1",
"name": "Agent",
"source": source_ref,
"tools": [],
"shared_publicly": True,
"shared_token": "tok",
}
mock_resolve = Mock(return_value=[])
mock_db = Mock()
mock_db.dereference.return_value = {"_id": source_id}
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
):
with app.test_request_context("/api/shared_agent?token=tok"):
from flask import request
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 200
assert response.json["source"] == str(source_id)
def test_returns_400_on_exception(self, app):
from application.api.user.agents.sharing import SharedAgent
mock_col = Mock()
mock_col.find_one.side_effect = Exception("DB error")
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context("/api/shared_agent?token=tok"):
response = SharedAgent().get()
assert response.status_code == 400
def test_tool_enrichment_handles_missing_tool(self, app):
from application.api.user.agents.sharing import SharedAgent
agent_id = ObjectId()
tool_id = str(ObjectId())
mock_agents_col = Mock()
mock_agents_col.find_one.return_value = {
"_id": agent_id,
"user": "owner1",
"name": "Agent",
"tools": [tool_id],
"shared_publicly": True,
"shared_token": "tok",
}
mock_tools_col = Mock()
mock_tools_col.find_one.return_value = None
mock_resolve = Mock(return_value=[])
mock_db = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.user_tools_collection", mock_tools_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
):
with app.test_request_context("/api/shared_agent?token=tok"):
from flask import request
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 200
# Missing tools are skipped
assert response.json["tools"] == []
def test_image_url_generated_when_present(self, app):
from application.api.user.agents.sharing import SharedAgent
agent_id = ObjectId()
mock_agents_col = Mock()
mock_agents_col.find_one.return_value = {
"_id": agent_id,
"user": "owner1",
"name": "Agent",
"image": "path/to/img.png",
"tools": [],
"shared_publicly": True,
"shared_token": "tok",
}
mock_resolve = Mock(return_value=[])
mock_db = Mock()
mock_generate = Mock(return_value="http://example.com/img.png")
with patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.db", mock_db
), patch(
"application.api.user.agents.sharing.generate_image_url", mock_generate
):
with app.test_request_context("/api/shared_agent?token=tok"):
from flask import request
request.decoded_token = None
response = SharedAgent().get()
assert response.status_code == 200
assert response.json["image"] == "http://example.com/img.png"
mock_generate.assert_called_once_with("path/to/img.png")
# ---------------------------------------------------------------------------
# SharedAgents (GET /shared_agents)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestSharedAgents:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.sharing import SharedAgents
with app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = None
response = SharedAgents().get()
assert response.status_code == 401
def test_returns_shared_agents_list(self, app):
from application.api.user.agents.sharing import SharedAgents
agent_id = ObjectId()
mock_ensure = Mock(
return_value={
"user_id": "user1",
"agent_preferences": {
"shared_with_me": [str(agent_id)],
"pinned": [str(agent_id)],
},
}
)
mock_agents_col = Mock()
mock_agents_col.find.return_value = [
{
"_id": agent_id,
"name": "Shared Agent",
"description": "desc",
"tools": [],
"agent_type": "classic",
"status": "published",
"shared_publicly": True,
"shared_token": "tok123",
}
]
mock_resolve = Mock(return_value=[])
mock_users_col = Mock()
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.users_collection", mock_users_col
):
with app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = SharedAgents().get()
assert response.status_code == 200
data = response.json
assert len(data) == 1
assert data[0]["name"] == "Shared Agent"
assert data[0]["pinned"] is True
def test_removes_stale_shared_ids(self, app):
from application.api.user.agents.sharing import SharedAgents
stale_id = str(ObjectId())
mock_ensure = Mock(
return_value={
"user_id": "user1",
"agent_preferences": {
"shared_with_me": [stale_id],
"pinned": [],
},
}
)
mock_agents_col = Mock()
mock_agents_col.find.return_value = [] # None found
mock_users_col = Mock()
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.users_collection", mock_users_col
):
with app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = SharedAgents().get()
assert response.status_code == 200
mock_users_col.update_one.assert_called_once()
call_args = mock_users_col.update_one.call_args
assert stale_id in call_args[0][1]["$pullAll"][
"agent_preferences.shared_with_me"
]
def test_returns_empty_when_no_shared_ids(self, app):
from application.api.user.agents.sharing import SharedAgents
mock_ensure = Mock(
return_value={
"user_id": "user1",
"agent_preferences": {"shared_with_me": [], "pinned": []},
}
)
mock_agents_col = Mock()
mock_agents_col.find.return_value = []
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
):
with app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = SharedAgents().get()
assert response.status_code == 200
assert response.json == []
def test_returns_400_on_exception(self, app):
from application.api.user.agents.sharing import SharedAgents
mock_ensure = Mock(side_effect=Exception("DB error"))
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
):
with app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = SharedAgents().get()
assert response.status_code == 400
def test_image_url_generated(self, app):
from application.api.user.agents.sharing import SharedAgents
agent_id = ObjectId()
mock_ensure = Mock(
return_value={
"user_id": "user1",
"agent_preferences": {
"shared_with_me": [str(agent_id)],
"pinned": [],
},
}
)
mock_agents_col = Mock()
mock_agents_col.find.return_value = [
{
"_id": agent_id,
"name": "Agent",
"image": "path.png",
"tools": [],
"shared_publicly": True,
}
]
mock_resolve = Mock(return_value=[])
mock_generate = Mock(return_value="http://example.com/path.png")
with patch(
"application.api.user.agents.sharing.ensure_user_doc", mock_ensure
), patch(
"application.api.user.agents.sharing.agents_collection", mock_agents_col
), patch(
"application.api.user.agents.sharing.resolve_tool_details", mock_resolve
), patch(
"application.api.user.agents.sharing.generate_image_url", mock_generate
), patch(
"application.api.user.agents.sharing.users_collection", Mock()
):
with app.test_request_context("/api/shared_agents"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = SharedAgents().get()
assert response.status_code == 200
assert response.json[0]["image"] == "http://example.com/path.png"
# ---------------------------------------------------------------------------
# ShareAgent (PUT /share_agent)
# ---------------------------------------------------------------------------
@pytest.mark.unit
class TestShareAgent:
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={"id": "abc", "shared": True},
):
from flask import request
request.decoded_token = None
response = ShareAgent().put()
assert response.status_code == 401
def test_returns_400_missing_json_body(self, app):
from application.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
method="PUT",
content_type="application/json",
data=b"{}",
):
from flask import request
request.decoded_token = {"sub": "user1"}
# Empty JSON object -> no id, no shared -> 400
response = ShareAgent().put()
assert response.status_code == 400
assert response.json["success"] is False
def test_returns_400_missing_id(self, app):
from application.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={"shared": True},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 400
def test_returns_400_missing_shared_param(self, app):
from application.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={"id": str(ObjectId())},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 400
def test_returns_400_invalid_agent_id(self, app):
from application.api.user.agents.sharing import ShareAgent
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={"id": "invalid-oid", "shared": True},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app):
from application.api.user.agents.sharing import ShareAgent
mock_col = Mock()
mock_col.find_one.return_value = None
agent_id = str(ObjectId())
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={"id": agent_id, "shared": True},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 404
def test_shares_agent_success(self, app):
from application.api.user.agents.sharing import ShareAgent
agent_id = ObjectId()
mock_col = Mock()
mock_col.find_one.return_value = {
"_id": agent_id,
"user": "user1",
}
mock_col.update_one.return_value = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={
"id": str(agent_id),
"shared": True,
"username": "TestUser",
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 200
data = response.json
assert data["success"] is True
assert data["shared_token"] is not None
mock_col.update_one.assert_called_once()
def test_unshares_agent_success(self, app):
from application.api.user.agents.sharing import ShareAgent
agent_id = ObjectId()
mock_col = Mock()
mock_col.find_one.return_value = {
"_id": agent_id,
"user": "user1",
}
mock_col.update_one.return_value = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={
"id": str(agent_id),
"shared": False,
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 200
data = response.json
assert data["success"] is True
assert data["shared_token"] is None
def test_returns_400_on_db_exception(self, app):
from application.api.user.agents.sharing import ShareAgent
agent_id = ObjectId()
mock_col = Mock()
mock_col.find_one.return_value = {
"_id": agent_id,
"user": "user1",
}
mock_col.update_one.side_effect = Exception("DB error")
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={
"id": str(agent_id),
"shared": True,
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 400
def test_share_with_username(self, app):
from application.api.user.agents.sharing import ShareAgent
agent_id = ObjectId()
mock_col = Mock()
mock_col.find_one.return_value = {
"_id": agent_id,
"user": "user1",
}
mock_col.update_one.return_value = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={
"id": str(agent_id),
"shared": True,
"username": "SharedByUser",
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 200
# Verify the update call includes shared_metadata with username
update_call = mock_col.update_one.call_args[0][1]["$set"]
assert update_call["shared_metadata"]["shared_by"] == "SharedByUser"
assert update_call["shared_publicly"] is True
assert "shared_token" in update_call
def test_shared_false_explicitly(self, app):
from application.api.user.agents.sharing import ShareAgent
agent_id = ObjectId()
mock_col = Mock()
mock_col.find_one.return_value = {
"_id": agent_id,
"user": "user1",
}
mock_col.update_one.return_value = Mock()
with patch(
"application.api.user.agents.sharing.agents_collection", mock_col
):
with app.test_request_context(
"/api/share_agent",
method="PUT",
json={
"id": str(agent_id),
"shared": False,
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareAgent().put()
assert response.status_code == 200
update_call = mock_col.update_one.call_args[0][1]
assert update_call["$set"]["shared_publicly"] is False
assert update_call["$set"]["shared_token"] is None
+388
View File
@@ -0,0 +1,388 @@
import datetime
from unittest.mock import Mock, patch
import pytest
from bson import ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
@pytest.mark.unit
class TestGetMessageAnalytics:
def test_returns_message_analytics_last_30_days(self, app):
from application.api.user.analytics.routes import GetMessageAnalytics
mock_conversations = Mock()
mock_conversations.aggregate.return_value = [
{"_id": "2024-06-01", "count": 5},
{"_id": "2024-06-02", "count": 3},
]
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_30_days"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetMessageAnalytics().post()
assert response.status_code == 200
assert response.json["success"] is True
assert "messages" in response.json
def test_returns_401_unauthenticated(self, app):
from application.api.user.analytics.routes import GetMessageAnalytics
with app.test_request_context(
"/api/get_message_analytics",
method="POST",
json={"filter_option": "last_30_days"},
):
from flask import request
request.decoded_token = None
response = GetMessageAnalytics().post()
assert response.status_code == 401
def test_returns_400_invalid_filter_option(self, app):
from application.api.user.analytics.routes import GetMessageAnalytics
mock_agents = Mock()
mock_agents.find_one.return_value = None
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": "invalid_option"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetMessageAnalytics().post()
assert response.status_code == 400
def test_filters_by_api_key(self, app):
from application.api.user.analytics.routes import GetMessageAnalytics
agent_id = ObjectId()
mock_agents = Mock()
mock_agents.find_one.return_value = {
"_id": agent_id,
"key": "api_key_value",
}
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_message_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 = GetMessageAnalytics().post()
assert response.status_code == 200
pipeline = mock_conversations.aggregate.call_args[0][0]
assert pipeline[0]["$match"].get("api_key") == "api_key_value"
def test_last_hour_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_hour"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetMessageAnalytics().post()
assert response.status_code == 200
def test_last_24_hour_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_24_hour"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetMessageAnalytics().post()
assert response.status_code == 200
@pytest.mark.unit
class TestGetTokenAnalytics:
def test_returns_token_analytics(self, app):
from application.api.user.analytics.routes import GetTokenAnalytics
mock_token_usage = Mock()
mock_token_usage.aggregate.return_value = [
{"_id": {"day": "2024-06-01"}, "total_tokens": 1000}
]
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_30_days"},
):
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_returns_400_invalid_filter(self, app):
from application.api.user.analytics.routes import GetTokenAnalytics
mock_agents = Mock()
mock_agents.find_one.return_value = None
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": "invalid"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetTokenAnalytics().post()
assert response.status_code == 400
@pytest.mark.unit
class TestGetFeedbackAnalytics:
def test_returns_feedback_analytics(self, app):
from application.api.user.analytics.routes import GetFeedbackAnalytics
mock_conversations = Mock()
mock_conversations.aggregate.return_value = [
{"_id": "2024-06-01", "positive": 10, "negative": 2}
]
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_30_days"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetFeedbackAnalytics().post()
assert response.status_code == 200
assert response.json["success"] is True
assert "feedback" in response.json
def test_returns_400_invalid_filter(self, app):
from application.api.user.analytics.routes import GetFeedbackAnalytics
mock_agents = Mock()
mock_agents.find_one.return_value = None
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": "bad"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetFeedbackAnalytics().post()
assert response.status_code == 400
@pytest.mark.unit
class TestGetUserLogs:
def test_returns_paginated_logs(self, app):
from application.api.user.analytics.routes import GetUserLogs
log_id = ObjectId()
mock_cursor = Mock()
mock_cursor.sort.return_value.skip.return_value.limit.return_value = [
{
"_id": log_id,
"action": "query",
"level": "info",
"user": "user1",
"question": "test?",
"sources": [],
"retriever_params": {},
"timestamp": datetime.datetime(2024, 6, 1),
}
]
mock_user_logs = Mock()
mock_user_logs.find.return_value = mock_cursor
mock_agents = Mock()
mock_agents.find_one.return_value = None
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},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetUserLogs().post()
assert response.status_code == 200
assert response.json["success"] is True
assert response.json["page"] == 1
assert len(response.json["logs"]) == 1
assert response.json["has_more"] is False
def test_detects_has_more(self, app):
from application.api.user.analytics.routes import GetUserLogs
items = [
{"_id": ObjectId(), "action": f"q{i}", "level": "info"}
for i in range(3)
]
mock_cursor = Mock()
mock_cursor.sort.return_value.skip.return_value.limit.return_value = items
mock_user_logs = Mock()
mock_user_logs.find.return_value = mock_cursor
mock_agents = Mock()
mock_agents.find_one.return_value = None
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": 2},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetUserLogs().post()
assert response.status_code == 200
assert response.json["has_more"] is True
assert len(response.json["logs"]) == 2
def test_returns_401_unauthenticated(self, app):
from application.api.user.analytics.routes import GetUserLogs
with app.test_request_context(
"/api/get_user_logs",
method="POST",
json={"page": 1},
):
from flask import request
request.decoded_token = None
response = GetUserLogs().post()
assert response.status_code == 401
+360
View File
@@ -0,0 +1,360 @@
from unittest.mock import Mock, patch
import pytest
from bson import ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
@pytest.mark.unit
class TestDeleteConversation:
def test_deletes_conversation(self, app):
from application.api.user.conversations.routes import DeleteConversation
conv_id = ObjectId()
mock_collection = Mock()
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_collection,
):
with app.test_request_context(f"/api/delete_conversation?id={conv_id}"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = DeleteConversation().post()
assert response.status_code == 200
assert response.json["success"] is True
mock_collection.delete_one.assert_called_once_with(
{"_id": conv_id, "user": "user1"}
)
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import DeleteConversation
with app.test_request_context("/api/delete_conversation?id=abc"):
from flask import request
request.decoded_token = None
response = DeleteConversation().post()
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.conversations.routes import DeleteConversation
with app.test_request_context("/api/delete_conversation"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = DeleteConversation().post()
assert response.status_code == 400
@pytest.mark.unit
class TestDeleteAllConversations:
def test_deletes_all_for_user(self, app):
from application.api.user.conversations.routes import DeleteAllConversations
mock_collection = Mock()
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_collection,
):
with app.test_request_context("/api/delete_all_conversations"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = DeleteAllConversations().get()
assert response.status_code == 200
mock_collection.delete_many.assert_called_once_with({"user": "user1"})
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import DeleteAllConversations
with app.test_request_context("/api/delete_all_conversations"):
from flask import request
request.decoded_token = None
response = DeleteAllConversations().get()
assert response.status_code == 401
@pytest.mark.unit
class TestGetConversations:
def test_returns_conversations(self, app):
from application.api.user.conversations.routes import GetConversations
conv_id = ObjectId()
mock_cursor = Mock()
mock_cursor.sort.return_value.limit.return_value = [
{
"_id": conv_id,
"name": "Test Chat",
"agent_id": "agent1",
"is_shared_usage": False,
}
]
mock_collection = Mock()
mock_collection.find.return_value = mock_cursor
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_collection,
):
with app.test_request_context("/api/get_conversations"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetConversations().get()
assert response.status_code == 200
data = response.json
assert len(data) == 1
assert data[0]["id"] == str(conv_id)
assert data[0]["name"] == "Test Chat"
def test_returns_401_unauthenticated(self, app):
from application.api.user.conversations.routes import GetConversations
with app.test_request_context("/api/get_conversations"):
from flask import request
request.decoded_token = None
response = GetConversations().get()
assert response.status_code == 401
@pytest.mark.unit
class TestGetSingleConversation:
def test_returns_conversation(self, app):
from application.api.user.conversations.routes import GetSingleConversation
conv_id = ObjectId()
mock_conv_collection = Mock()
mock_conv_collection.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [{"prompt": "hi", "response": "hello"}],
"agent_id": "agent1",
}
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_conv_collection,
):
with app.test_request_context(
f"/api/get_single_conversation?id={conv_id}"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSingleConversation().get()
assert response.status_code == 200
assert response.json["queries"] == [{"prompt": "hi", "response": "hello"}]
assert response.json["agent_id"] == "agent1"
def test_returns_404_not_found(self, app):
from application.api.user.conversations.routes import GetSingleConversation
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_collection,
):
with app.test_request_context(
f"/api/get_single_conversation?id={ObjectId()}"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSingleConversation().get()
assert response.status_code == 404
def test_returns_400_missing_id(self, app):
from application.api.user.conversations.routes import GetSingleConversation
with app.test_request_context("/api/get_single_conversation"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSingleConversation().get()
assert response.status_code == 400
def test_resolves_attachments(self, app):
from application.api.user.conversations.routes import GetSingleConversation
conv_id = ObjectId()
att_id = ObjectId()
mock_conv_collection = Mock()
mock_conv_collection.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [
{"prompt": "hi", "response": "hello", "attachments": [str(att_id)]}
],
}
mock_att_collection = Mock()
mock_att_collection.find_one.return_value = {
"_id": att_id,
"filename": "doc.pdf",
}
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_conv_collection,
), patch(
"application.api.user.conversations.routes.attachments_collection",
mock_att_collection,
):
with app.test_request_context(
f"/api/get_single_conversation?id={conv_id}"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSingleConversation().get()
assert response.status_code == 200
attachments = response.json["queries"][0]["attachments"]
assert len(attachments) == 1
assert attachments[0]["fileName"] == "doc.pdf"
@pytest.mark.unit
class TestUpdateConversationName:
def test_updates_name(self, app):
from application.api.user.conversations.routes import UpdateConversationName
conv_id = ObjectId()
mock_collection = Mock()
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_collection,
):
with app.test_request_context(
"/api/update_conversation_name",
method="POST",
json={"id": str(conv_id), "name": "New Name"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = UpdateConversationName().post()
assert response.status_code == 200
assert response.json["success"] is True
mock_collection.update_one.assert_called_once()
def test_returns_400_missing_fields(self, app):
from application.api.user.conversations.routes import UpdateConversationName
with app.test_request_context(
"/api/update_conversation_name",
method="POST",
json={"id": str(ObjectId())},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = UpdateConversationName().post()
assert response.status_code == 400
@pytest.mark.unit
class TestSubmitFeedback:
def test_submits_positive_feedback(self, app):
from application.api.user.conversations.routes import SubmitFeedback
conv_id = ObjectId()
mock_collection = Mock()
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_collection,
):
with app.test_request_context(
"/api/feedback",
method="POST",
json={
"feedback": "LIKE",
"conversation_id": str(conv_id),
"question_index": 0,
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = SubmitFeedback().post()
assert response.status_code == 200
assert response.json["success"] is True
call_args = mock_collection.update_one.call_args
assert "$set" in call_args[0][1]
def test_removes_feedback_when_null(self, app):
from application.api.user.conversations.routes import SubmitFeedback
conv_id = ObjectId()
mock_collection = Mock()
with patch(
"application.api.user.conversations.routes.conversations_collection",
mock_collection,
):
with app.test_request_context(
"/api/feedback",
method="POST",
json={
"feedback": None,
"conversation_id": str(conv_id),
"question_index": 0,
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = SubmitFeedback().post()
assert response.status_code == 200
call_args = mock_collection.update_one.call_args
assert "$unset" in call_args[0][1]
def test_returns_400_missing_fields(self, app):
from application.api.user.conversations.routes import SubmitFeedback
with app.test_request_context(
"/api/feedback",
method="POST",
json={"feedback": "LIKE"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = SubmitFeedback().post()
assert response.status_code == 400
+509
View File
@@ -0,0 +1,509 @@
import datetime
from unittest.mock import Mock, patch
import pytest
from bson import ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
@pytest.mark.unit
class TestAgentFoldersGet:
def test_returns_folders(self, app):
from application.api.user.agents.folders import AgentFolders
now = datetime.datetime(2024, 6, 15, tzinfo=datetime.timezone.utc)
folder_id = ObjectId()
mock_collection = Mock()
mock_collection.find.return_value = [
{
"_id": folder_id,
"name": "My Folder",
"parent_id": None,
"created_at": now,
"updated_at": now,
}
]
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context("/api/agents/folders/", method="GET"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolders().get()
assert response.status_code == 200
folders = response.json["folders"]
assert len(folders) == 1
assert folders[0]["id"] == str(folder_id)
assert folders[0]["name"] == "My Folder"
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.folders import AgentFolders
with app.test_request_context("/api/agents/folders/", method="GET"):
from flask import request
request.decoded_token = None
response = AgentFolders().get()
assert response.status_code == 401
@pytest.mark.unit
class TestAgentFoldersCreate:
def test_creates_folder(self, app):
from application.api.user.agents.folders import AgentFolders
inserted_id = ObjectId()
mock_collection = Mock()
mock_collection.insert_one.return_value = Mock(inserted_id=inserted_id)
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
"/api/agents/folders/",
method="POST",
json={"name": "New Folder"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolders().post()
assert response.status_code == 201
assert response.json["id"] == str(inserted_id)
assert response.json["name"] == "New Folder"
def test_returns_400_missing_name(self, app):
from application.api.user.agents.folders import AgentFolders
with app.test_request_context(
"/api/agents/folders/",
method="POST",
json={},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolders().post()
assert response.status_code == 400
def test_validates_parent_folder_exists(self, app):
from application.api.user.agents.folders import AgentFolders
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
"/api/agents/folders/",
method="POST",
json={"name": "Sub", "parent_id": str(ObjectId())},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolders().post()
assert response.status_code == 404
@pytest.mark.unit
class TestAgentFolderGet:
def test_returns_folder_with_agents_and_subfolders(self, app):
from application.api.user.agents.folders import AgentFolder
folder_id = ObjectId()
agent_id = ObjectId()
subfolder_id = ObjectId()
mock_folders = Mock()
mock_folders.find_one.return_value = {
"_id": folder_id,
"name": "Folder",
"parent_id": None,
}
mock_folders.find.return_value = [
{"_id": subfolder_id, "name": "Subfolder"}
]
mock_agents = Mock()
mock_agents.find.return_value = [
{"_id": agent_id, "name": "Agent 1", "description": "Desc"}
]
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_folders,
), patch(
"application.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
f"/api/agents/folders/{folder_id}", method="GET"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolder().get(str(folder_id))
assert response.status_code == 200
assert response.json["name"] == "Folder"
assert len(response.json["agents"]) == 1
assert len(response.json["subfolders"]) == 1
def test_returns_404_not_found(self, app):
from application.api.user.agents.folders import AgentFolder
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
f"/api/agents/folders/{ObjectId()}", method="GET"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolder().get(str(ObjectId()))
assert response.status_code == 404
@pytest.mark.unit
class TestAgentFolderUpdate:
def test_updates_folder_name(self, app):
from application.api.user.agents.folders import AgentFolder
folder_id = ObjectId()
mock_collection = Mock()
mock_collection.update_one.return_value = Mock(matched_count=1)
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
f"/api/agents/folders/{folder_id}",
method="PUT",
json={"name": "Renamed"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolder().put(str(folder_id))
assert response.status_code == 200
assert response.json["success"] is True
def test_prevents_self_parent(self, app):
from application.api.user.agents.folders import AgentFolder
folder_id = str(ObjectId())
with app.test_request_context(
f"/api/agents/folders/{folder_id}",
method="PUT",
json={"parent_id": folder_id},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolder().put(folder_id)
assert response.status_code == 400
assert "own parent" in response.json["message"]
def test_returns_404_when_not_found(self, app):
from application.api.user.agents.folders import AgentFolder
mock_collection = Mock()
mock_collection.update_one.return_value = Mock(matched_count=0)
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_collection,
):
with app.test_request_context(
f"/api/agents/folders/{ObjectId()}",
method="PUT",
json={"name": "X"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolder().put(str(ObjectId()))
assert response.status_code == 404
@pytest.mark.unit
class TestAgentFolderDelete:
def test_deletes_folder_and_unsets_references(self, app):
from application.api.user.agents.folders import AgentFolder
folder_id = str(ObjectId())
mock_folders = Mock()
mock_folders.delete_one.return_value = Mock(deleted_count=1)
mock_agents = Mock()
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_folders,
), patch(
"application.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
f"/api/agents/folders/{folder_id}", method="DELETE"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolder().delete(folder_id)
assert response.status_code == 200
mock_agents.update_many.assert_called_once()
mock_folders.update_many.assert_called_once()
mock_folders.delete_one.assert_called_once()
def test_returns_404_not_found(self, app):
from application.api.user.agents.folders import AgentFolder
mock_folders = Mock()
mock_folders.delete_one.return_value = Mock(deleted_count=0)
mock_agents = Mock()
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_folders,
), patch(
"application.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
f"/api/agents/folders/{ObjectId()}", method="DELETE"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentFolder().delete(str(ObjectId()))
assert response.status_code == 404
@pytest.mark.unit
class TestMoveAgentToFolder:
def test_moves_agent_to_folder(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
agent_id = ObjectId()
folder_id = ObjectId()
mock_agents = Mock()
mock_agents.find_one.return_value = {"_id": agent_id, "user": "user1"}
mock_folders = Mock()
mock_folders.find_one.return_value = {"_id": folder_id}
with patch(
"application.api.user.agents.folders.agents_collection",
mock_agents,
), patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_folders,
):
with app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={
"agent_id": str(agent_id),
"folder_id": str(folder_id),
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = MoveAgentToFolder().post()
assert response.status_code == 200
mock_agents.update_one.assert_called_once()
def test_removes_agent_from_folder(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
agent_id = ObjectId()
mock_agents = Mock()
mock_agents.find_one.return_value = {"_id": agent_id, "user": "user1"}
with patch(
"application.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={"agent_id": str(agent_id), "folder_id": None},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = MoveAgentToFolder().post()
assert response.status_code == 200
call_args = mock_agents.update_one.call_args
assert "$unset" in call_args[0][1]
def test_returns_404_agent_not_found(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
mock_agents = Mock()
mock_agents.find_one.return_value = None
with patch(
"application.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={"agent_id": str(ObjectId())},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = MoveAgentToFolder().post()
assert response.status_code == 404
def test_returns_400_missing_agent_id(self, app):
from application.api.user.agents.folders import MoveAgentToFolder
with app.test_request_context(
"/api/agents/folders/move_agent",
method="POST",
json={},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = MoveAgentToFolder().post()
assert response.status_code == 400
@pytest.mark.unit
class TestBulkMoveAgents:
def test_bulk_moves_to_folder(self, app):
from application.api.user.agents.folders import BulkMoveAgents
folder_id = ObjectId()
agent_ids = [str(ObjectId()), str(ObjectId())]
mock_agents = Mock()
mock_folders = Mock()
mock_folders.find_one.return_value = {"_id": folder_id}
with patch(
"application.api.user.agents.folders.agents_collection",
mock_agents,
), patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_folders,
):
with app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={"agent_ids": agent_ids, "folder_id": str(folder_id)},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = BulkMoveAgents().post()
assert response.status_code == 200
mock_agents.update_many.assert_called_once()
def test_bulk_removes_from_folders(self, app):
from application.api.user.agents.folders import BulkMoveAgents
agent_ids = [str(ObjectId())]
mock_agents = Mock()
with patch(
"application.api.user.agents.folders.agents_collection",
mock_agents,
):
with app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={"agent_ids": agent_ids},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = BulkMoveAgents().post()
assert response.status_code == 200
call_args = mock_agents.update_many.call_args
assert "$unset" in call_args[0][1]
def test_returns_400_missing_agent_ids(self, app):
from application.api.user.agents.folders import BulkMoveAgents
with app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = BulkMoveAgents().post()
assert response.status_code == 400
def test_returns_404_folder_not_found(self, app):
from application.api.user.agents.folders import BulkMoveAgents
mock_folders = Mock()
mock_folders.find_one.return_value = None
with patch(
"application.api.user.agents.folders.agent_folders_collection",
mock_folders,
):
with app.test_request_context(
"/api/agents/folders/bulk_move",
method="POST",
json={
"agent_ids": [str(ObjectId())],
"folder_id": str(ObjectId()),
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = BulkMoveAgents().post()
assert response.status_code == 404
+70
View File
@@ -0,0 +1,70 @@
from unittest.mock import Mock, patch
import pytest
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
@pytest.mark.unit
class TestModelsListResource:
def test_returns_models(self, app):
from application.api.user.models.routes import ModelsListResource
mock_model = Mock()
mock_model.to_dict.return_value = {
"id": "gpt-4",
"name": "GPT-4",
"provider": "openai",
}
mock_registry = Mock()
mock_registry.get_enabled_models.return_value = [mock_model]
mock_registry.default_model_id = "gpt-4"
with patch(
"application.api.user.models.routes.ModelRegistry.get_instance",
return_value=mock_registry,
):
with app.test_request_context("/api/models"):
response = ModelsListResource().get()
assert response.status_code == 200
assert response.json["count"] == 1
assert response.json["default_model_id"] == "gpt-4"
assert response.json["models"][0]["id"] == "gpt-4"
def test_returns_empty_models(self, app):
from application.api.user.models.routes import ModelsListResource
mock_registry = Mock()
mock_registry.get_enabled_models.return_value = []
mock_registry.default_model_id = None
with patch(
"application.api.user.models.routes.ModelRegistry.get_instance",
return_value=mock_registry,
):
with app.test_request_context("/api/models"):
response = ModelsListResource().get()
assert response.status_code == 200
assert response.json["count"] == 0
assert response.json["models"] == []
def test_returns_500_on_error(self, app):
from application.api.user.models.routes import ModelsListResource
with patch(
"application.api.user.models.routes.ModelRegistry.get_instance",
side_effect=Exception("Registry error"),
):
with app.test_request_context("/api/models"):
response = ModelsListResource().get()
assert response.status_code == 500
+288
View File
@@ -0,0 +1,288 @@
from unittest.mock import Mock, mock_open, patch
import pytest
from bson import ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
@pytest.mark.unit
class TestCreatePrompt:
def test_creates_prompt(self, app):
from application.api.user.prompts.routes import CreatePrompt
mock_collection = Mock()
inserted_id = ObjectId()
mock_collection.insert_one.return_value = Mock(inserted_id=inserted_id)
with patch(
"application.api.user.prompts.routes.prompts_collection",
mock_collection,
):
with app.test_request_context(
"/api/create_prompt",
method="POST",
json={"name": "My Prompt", "content": "You are helpful."},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = CreatePrompt().post()
assert response.status_code == 200
assert response.json["id"] == str(inserted_id)
mock_collection.insert_one.assert_called_once()
doc = mock_collection.insert_one.call_args[0][0]
assert doc["name"] == "My Prompt"
assert doc["user"] == "user1"
def test_returns_401_unauthenticated(self, app):
from application.api.user.prompts.routes import CreatePrompt
with app.test_request_context(
"/api/create_prompt",
method="POST",
json={"name": "P", "content": "C"},
):
from flask import request
request.decoded_token = None
response = CreatePrompt().post()
assert response.status_code == 401
def test_returns_400_missing_fields(self, app):
from application.api.user.prompts.routes import CreatePrompt
with app.test_request_context(
"/api/create_prompt",
method="POST",
json={"name": "P"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = CreatePrompt().post()
assert response.status_code == 400
@pytest.mark.unit
class TestGetPrompts:
def test_returns_prompts_with_defaults(self, app):
from application.api.user.prompts.routes import GetPrompts
user_prompt_id = ObjectId()
mock_collection = Mock()
mock_collection.find.return_value = [
{"_id": user_prompt_id, "name": "Custom Prompt"}
]
with patch(
"application.api.user.prompts.routes.prompts_collection",
mock_collection,
):
with app.test_request_context("/api/get_prompts"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetPrompts().get()
assert response.status_code == 200
data = response.json
public_names = [p["name"] for p in data if p["type"] == "public"]
assert "default" in public_names
assert "creative" in public_names
assert "strict" in public_names
private = [p for p in data if p["type"] == "private"]
assert len(private) == 1
assert private[0]["name"] == "Custom Prompt"
def test_returns_401_unauthenticated(self, app):
from application.api.user.prompts.routes import GetPrompts
with app.test_request_context("/api/get_prompts"):
from flask import request
request.decoded_token = None
response = GetPrompts().get()
assert response.status_code == 401
@pytest.mark.unit
class TestGetSinglePrompt:
def test_returns_default_prompt(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
with patch("builtins.open", mock_open(read_data="Default prompt content")):
with app.test_request_context("/api/get_single_prompt?id=default"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSinglePrompt().get()
assert response.status_code == 200
assert response.json["content"] == "Default prompt content"
def test_returns_creative_prompt(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
with patch("builtins.open", mock_open(read_data="Creative content")):
with app.test_request_context("/api/get_single_prompt?id=creative"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSinglePrompt().get()
assert response.status_code == 200
assert response.json["content"] == "Creative content"
def test_returns_strict_prompt(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
with patch("builtins.open", mock_open(read_data="Strict content")):
with app.test_request_context("/api/get_single_prompt?id=strict"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSinglePrompt().get()
assert response.status_code == 200
assert response.json["content"] == "Strict content"
def test_returns_custom_prompt(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
prompt_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": prompt_id,
"content": "Custom content",
}
with patch(
"application.api.user.prompts.routes.prompts_collection",
mock_collection,
):
with app.test_request_context(
f"/api/get_single_prompt?id={prompt_id}"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSinglePrompt().get()
assert response.status_code == 200
assert response.json["content"] == "Custom content"
def test_returns_400_missing_id(self, app):
from application.api.user.prompts.routes import GetSinglePrompt
with app.test_request_context("/api/get_single_prompt"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = GetSinglePrompt().get()
assert response.status_code == 400
@pytest.mark.unit
class TestDeletePrompt:
def test_deletes_prompt(self, app):
from application.api.user.prompts.routes import DeletePrompt
prompt_id = ObjectId()
mock_collection = Mock()
with patch(
"application.api.user.prompts.routes.prompts_collection",
mock_collection,
):
with app.test_request_context(
"/api/delete_prompt",
method="POST",
json={"id": str(prompt_id)},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = DeletePrompt().post()
assert response.status_code == 200
assert response.json["success"] is True
mock_collection.delete_one.assert_called_once_with(
{"_id": prompt_id, "user": "user1"}
)
def test_returns_400_missing_id(self, app):
from application.api.user.prompts.routes import DeletePrompt
with app.test_request_context(
"/api/delete_prompt",
method="POST",
json={},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = DeletePrompt().post()
assert response.status_code == 400
@pytest.mark.unit
class TestUpdatePrompt:
def test_updates_prompt(self, app):
from application.api.user.prompts.routes import UpdatePrompt
prompt_id = ObjectId()
mock_collection = Mock()
with patch(
"application.api.user.prompts.routes.prompts_collection",
mock_collection,
):
with app.test_request_context(
"/api/update_prompt",
method="POST",
json={
"id": str(prompt_id),
"name": "Updated",
"content": "New content",
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = UpdatePrompt().post()
assert response.status_code == 200
assert response.json["success"] is True
mock_collection.update_one.assert_called_once()
def test_returns_400_missing_fields(self, app):
from application.api.user.prompts.routes import UpdatePrompt
with app.test_request_context(
"/api/update_prompt",
method="POST",
json={"id": str(ObjectId()), "name": "Updated"},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = UpdatePrompt().post()
assert response.status_code == 400
+690
View File
@@ -0,0 +1,690 @@
import uuid
from unittest.mock import Mock, patch
import pytest
from bson import ObjectId
from bson.binary import Binary, UuidRepresentation
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
@pytest.mark.unit
class TestShareConversation:
def test_shares_non_promptable_conversation(self, app):
from application.api.user.sharing.routes import ShareConversation
conv_id = ObjectId()
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Test Chat",
"queries": [{"prompt": "hi"}],
}
mock_shared = Mock()
mock_shared.find_one.return_value = None
mock_shared.insert_one.return_value = Mock()
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?isPromptable=false",
method="POST",
json={"conversation_id": str(conv_id)},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareConversation().post()
assert response.status_code == 201
assert response.json["success"] is True
assert "identifier" in response.json
mock_shared.insert_one.assert_called_once()
def test_returns_existing_shared_link(self, app):
from application.api.user.sharing.routes import ShareConversation
conv_id = ObjectId()
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Test Chat",
"queries": [{"prompt": "hi"}],
}
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": conv_id,
}
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?isPromptable=false",
method="POST",
json={"conversation_id": str(conv_id)},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareConversation().post()
assert response.status_code == 200
assert response.json["identifier"] == str(test_uuid)
def test_returns_401_unauthenticated(self, app):
from application.api.user.sharing.routes import ShareConversation
with app.test_request_context(
"/api/share?isPromptable=false",
method="POST",
json={"conversation_id": str(ObjectId())},
):
from flask import request
request.decoded_token = None
response = ShareConversation().post()
assert response.status_code == 401
def test_returns_400_missing_conversation_id(self, app):
from application.api.user.sharing.routes import ShareConversation
with app.test_request_context(
"/api/share?isPromptable=false",
method="POST",
json={},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareConversation().post()
assert response.status_code == 400
def test_returns_400_missing_isPromptable(self, app):
from application.api.user.sharing.routes import ShareConversation
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
assert "isPromptable" in response.json["message"]
def test_returns_404_conversation_not_found(self, app):
from application.api.user.sharing.routes import ShareConversation
mock_conversations = Mock()
mock_conversations.find_one.return_value = None
with patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
"/api/share?isPromptable=false",
method="POST",
json={"conversation_id": str(ObjectId())},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareConversation().post()
assert response.status_code == 404
@pytest.mark.unit
class TestGetPubliclySharedConversations:
def test_returns_shared_conversation(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": conv_id,
"first_n_queries": 2,
"isPromptable": False,
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Shared Chat",
"queries": [
{"prompt": "q1", "response": "a1"},
{"prompt": "q2", "response": "a2"},
{"prompt": "q3", "response": "a3"},
],
}
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 200
assert response.json["success"] is True
assert response.json["title"] == "Shared Chat"
assert len(response.json["queries"]) == 2
def test_returns_404_not_found(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
mock_shared = Mock()
mock_shared.find_one.return_value = None
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 404
def test_returns_404_conversation_deleted(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": conv_id,
"first_n_queries": 1,
"isPromptable": False,
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = None
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 404
def test_includes_api_key_when_promptable(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": conv_id,
"first_n_queries": 1,
"isPromptable": True,
"api_key": "shared_api_key",
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [{"prompt": "q1", "response": "a1"}],
}
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 200
assert response.json["api_key"] == "shared_api_key"
def test_handles_dbref_conversation_id(self, app):
from bson.dbref import DBRef
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": DBRef("conversations", conv_id),
"first_n_queries": 1,
"isPromptable": False,
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [{"prompt": "q1", "response": "a1"}],
}
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 200
mock_conversations.find_one.assert_called_once_with({"_id": conv_id})
def test_handles_dict_oid_conversation_id(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": {"$id": {"$oid": str(conv_id)}},
"first_n_queries": 1,
"isPromptable": False,
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [{"prompt": "q1", "response": "a1"}],
}
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 200
def test_handles_dict_id_string_conversation_id(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": {"$id": str(conv_id)},
"first_n_queries": 1,
"isPromptable": False,
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [{"prompt": "q1", "response": "a1"}],
}
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 200
def test_handles_dict_underscore_id_conversation_id(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": {"_id": str(conv_id)},
"first_n_queries": 1,
"isPromptable": False,
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [{"prompt": "q1", "response": "a1"}],
}
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 200
def test_handles_string_conversation_id(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": str(conv_id),
"first_n_queries": 1,
"isPromptable": False,
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [{"prompt": "q1", "response": "a1"}],
}
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 200
def test_resolves_attachments_in_shared(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
conv_id = ObjectId()
att_id = ObjectId()
mock_shared = Mock()
mock_shared.find_one.return_value = {
"uuid": binary_uuid,
"conversation_id": conv_id,
"first_n_queries": 1,
"isPromptable": False,
}
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Chat",
"queries": [
{"prompt": "q1", "response": "a1", "attachments": [str(att_id)]}
],
}
mock_attachments = Mock()
mock_attachments.find_one.return_value = {
"_id": att_id,
"filename": "file.pdf",
}
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
), patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
), patch(
"application.api.user.sharing.routes.attachments_collection",
mock_attachments,
):
with app.test_request_context(
f"/api/shared_conversation/{test_uuid}"
):
response = GetPubliclySharedConversations().get(str(test_uuid))
assert response.status_code == 200
assert response.json["queries"][0]["attachments"][0]["fileName"] == "file.pdf"
def test_handles_general_exception(self, app):
from application.api.user.sharing.routes import (
GetPubliclySharedConversations,
)
mock_shared = Mock()
mock_shared.find_one.side_effect = Exception("DB error")
with patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
):
with app.test_request_context(
f"/api/shared_conversation/{uuid.uuid4()}"
):
response = GetPubliclySharedConversations().get(str(uuid.uuid4()))
assert response.status_code == 400
@pytest.mark.unit
class TestShareConversationPromptable:
def test_promptable_with_existing_api_key_and_existing_share(self, app):
from application.api.user.sharing.routes import ShareConversation
conv_id = ObjectId()
test_uuid = uuid.uuid4()
binary_uuid = Binary.from_uuid(test_uuid, UuidRepresentation.STANDARD)
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Test Chat",
"queries": [{"prompt": "hi"}],
}
mock_agents = Mock()
mock_agents.find_one.return_value = {"key": "existing_api_uuid"}
mock_shared = Mock()
mock_shared.find_one.return_value = {"uuid": binary_uuid}
with patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
), patch(
"application.api.user.sharing.routes.agents_collection",
mock_agents,
), patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
):
with app.test_request_context(
"/api/share?isPromptable=true",
method="POST",
json={
"conversation_id": str(conv_id),
"prompt_id": "default",
"chunks": "3",
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareConversation().post()
assert response.status_code == 200
assert response.json["identifier"] == str(test_uuid)
def test_promptable_with_existing_api_key_new_share(self, app):
from application.api.user.sharing.routes import ShareConversation
conv_id = ObjectId()
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Test Chat",
"queries": [{"prompt": "hi"}],
}
mock_agents = Mock()
mock_agents.find_one.return_value = {"key": "existing_api_uuid"}
mock_shared = Mock()
mock_shared.find_one.return_value = None
with patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
), patch(
"application.api.user.sharing.routes.agents_collection",
mock_agents,
), patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
):
with app.test_request_context(
"/api/share?isPromptable=true",
method="POST",
json={
"conversation_id": str(conv_id),
"source": str(ObjectId()),
"retriever": "classic",
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareConversation().post()
assert response.status_code == 201
mock_shared.insert_one.assert_called_once()
def test_promptable_creates_new_api_key(self, app):
from application.api.user.sharing.routes import ShareConversation
conv_id = ObjectId()
mock_conversations = Mock()
mock_conversations.find_one.return_value = {
"_id": conv_id,
"name": "Test Chat",
"queries": [{"prompt": "hi"}],
}
mock_agents = Mock()
mock_agents.find_one.return_value = None
mock_shared = Mock()
with patch(
"application.api.user.sharing.routes.conversations_collection",
mock_conversations,
), patch(
"application.api.user.sharing.routes.agents_collection",
mock_agents,
), patch(
"application.api.user.sharing.routes.shared_conversations_collections",
mock_shared,
):
with app.test_request_context(
"/api/share?isPromptable=true",
method="POST",
json={
"conversation_id": str(conv_id),
"source": str(ObjectId()),
"retriever": "classic",
},
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = ShareConversation().post()
assert response.status_code == 201
mock_agents.insert_one.assert_called_once()
mock_shared.insert_one.assert_called_once()
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+411
View File
@@ -0,0 +1,411 @@
from unittest.mock import Mock
import pytest
from bson import ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
@pytest.mark.unit
class TestGetUserId:
def test_returns_user_id_from_decoded_token(self, app):
from application.api.user.utils import get_user_id
with app.test_request_context():
from flask import request
request.decoded_token = {"sub": "user_123"}
assert get_user_id() == "user_123"
def test_returns_none_when_no_decoded_token(self, app):
from application.api.user.utils import get_user_id
with app.test_request_context():
assert get_user_id() is None
def test_returns_none_when_decoded_token_has_no_sub(self, app):
from application.api.user.utils import get_user_id
with app.test_request_context():
from flask import request
request.decoded_token = {}
assert get_user_id() is None
@pytest.mark.unit
class TestRequireAuth:
def test_allows_authenticated_request(self, app):
from application.api.user.utils import require_auth
@require_auth
def protected():
return "ok"
with app.test_request_context():
from flask import request
request.decoded_token = {"sub": "user_123"}
assert protected() == "ok"
def test_returns_401_when_unauthenticated(self, app):
from application.api.user.utils import require_auth
@require_auth
def protected():
return "ok"
with app.test_request_context():
result = protected()
assert result.status_code == 401
@pytest.mark.unit
class TestSuccessResponse:
def test_default_success_response(self, app):
from application.api.user.utils import success_response
with app.app_context():
resp = success_response()
assert resp.status_code == 200
assert resp.json["success"] is True
def test_success_response_with_data(self, app):
from application.api.user.utils import success_response
with app.app_context():
resp = success_response({"items": [1, 2], "total": 2})
assert resp.status_code == 200
assert resp.json["success"] is True
assert resp.json["items"] == [1, 2]
assert resp.json["total"] == 2
def test_success_response_custom_status(self, app):
from application.api.user.utils import success_response
with app.app_context():
resp = success_response({"id": "new"}, 201)
assert resp.status_code == 201
@pytest.mark.unit
class TestErrorResponse:
def test_default_error_response(self, app):
from application.api.user.utils import error_response
with app.app_context():
resp = error_response("Something went wrong")
assert resp.status_code == 400
assert resp.json["success"] is False
assert resp.json["message"] == "Something went wrong"
def test_error_response_custom_status(self, app):
from application.api.user.utils import error_response
with app.app_context():
resp = error_response("Not found", 404)
assert resp.status_code == 404
def test_error_response_extra_kwargs(self, app):
from application.api.user.utils import error_response
with app.app_context():
resp = error_response("Bad", 400, errors=["field1", "field2"])
assert resp.json["errors"] == ["field1", "field2"]
@pytest.mark.unit
class TestValidateObjectId:
def test_valid_object_id(self, app):
from application.api.user.utils import validate_object_id
with app.app_context():
oid = ObjectId()
result, error = validate_object_id(str(oid))
assert result == oid
assert error is None
def test_invalid_object_id(self, app):
from application.api.user.utils import validate_object_id
with app.app_context():
result, error = validate_object_id("not-a-valid-id")
assert result is None
assert error.status_code == 400
assert "Invalid" in error.json["message"]
def test_custom_resource_name(self, app):
from application.api.user.utils import validate_object_id
with app.app_context():
_, error = validate_object_id("bad", "Workflow")
assert "Workflow" in error.json["message"]
@pytest.mark.unit
class TestValidatePagination:
def test_default_pagination(self, app):
from application.api.user.utils import validate_pagination
with app.test_request_context("/?limit=10&skip=0"):
limit, skip, error = validate_pagination()
assert limit == 10
assert skip == 0
assert error is None
def test_uses_defaults_when_no_params(self, app):
from application.api.user.utils import validate_pagination
with app.test_request_context("/"):
limit, skip, error = validate_pagination()
assert limit == 20
assert skip == 0
assert error is None
def test_enforces_max_limit(self, app):
from application.api.user.utils import validate_pagination
with app.test_request_context("/?limit=500"):
limit, _, _ = validate_pagination(max_limit=100)
assert limit == 100
def test_invalid_limit(self, app):
from application.api.user.utils import validate_pagination
with app.test_request_context("/?limit=-1"):
_, _, error = validate_pagination()
assert error is not None
assert error.status_code == 400
def test_invalid_skip(self, app):
from application.api.user.utils import validate_pagination
with app.test_request_context("/?skip=-1"):
_, _, error = validate_pagination()
assert error is not None
def test_non_numeric_values(self, app):
from application.api.user.utils import validate_pagination
with app.test_request_context("/?limit=abc"):
_, _, error = validate_pagination()
assert error is not None
@pytest.mark.unit
class TestCheckResourceOwnership:
def test_returns_resource_when_owned(self, app):
from application.api.user.utils import check_resource_ownership
with app.app_context():
collection = Mock()
oid = ObjectId()
doc = {"_id": oid, "user": "user1", "name": "test"}
collection.find_one.return_value = doc
resource, error = check_resource_ownership(collection, oid, "user1")
assert resource == doc
assert error is None
def test_returns_404_when_not_found(self, app):
from application.api.user.utils import check_resource_ownership
with app.app_context():
collection = Mock()
collection.find_one.return_value = None
resource, error = check_resource_ownership(
collection, ObjectId(), "user1", "Workflow"
)
assert resource is None
assert error.status_code == 404
assert "Workflow" in error.json["message"]
@pytest.mark.unit
class TestSerializeObjectId:
def test_converts_id_to_string(self):
from application.api.user.utils import serialize_object_id
oid = ObjectId()
obj = {"_id": oid, "name": "test"}
result = serialize_object_id(obj)
assert result["id"] == str(oid)
assert "_id" not in result
def test_custom_field_names(self):
from application.api.user.utils import serialize_object_id
oid = ObjectId()
obj = {"custom_id": oid}
result = serialize_object_id(obj, id_field="custom_id", new_field="uid")
assert result["uid"] == str(oid)
assert "custom_id" not in result
def test_no_id_field_present(self):
from application.api.user.utils import serialize_object_id
obj = {"name": "test"}
result = serialize_object_id(obj)
assert "id" not in result
@pytest.mark.unit
class TestSerializeList:
def test_applies_serializer_to_all_items(self):
from application.api.user.utils import serialize_list
items = [{"_id": ObjectId()}, {"_id": ObjectId()}]
def serializer(item):
return {"id": str(item["_id"])}
result = serialize_list(items, serializer)
assert len(result) == 2
assert all("id" in r for r in result)
def test_empty_list(self):
from application.api.user.utils import serialize_list
assert serialize_list([], lambda x: x) == []
@pytest.mark.unit
class TestRequireFields:
def test_allows_valid_request(self, app):
from application.api.user.utils import require_fields
@require_fields(["name", "email"])
def handler():
return "ok"
with app.test_request_context(
"/", method="POST", json={"name": "Alice", "email": "a@b.com"}
):
assert handler() == "ok"
def test_rejects_missing_fields(self, app):
from application.api.user.utils import require_fields
@require_fields(["name", "email"])
def handler():
return "ok"
with app.test_request_context("/", method="POST", json={"name": "Alice"}):
result = handler()
assert result.status_code == 400
assert "email" in result.json["message"]
def test_rejects_empty_body(self, app):
from application.api.user.utils import require_fields
@require_fields(["name"])
def handler():
return "ok"
with app.test_request_context(
"/", method="POST", json={}
):
result = handler()
assert result.status_code == 400
@pytest.mark.unit
class TestSafeDbOperation:
def test_returns_result_on_success(self, app):
from application.api.user.utils import safe_db_operation
with app.app_context():
result, error = safe_db_operation(lambda: {"inserted": True})
assert result == {"inserted": True}
assert error is None
def test_returns_error_on_exception(self, app):
from application.api.user.utils import safe_db_operation
with app.app_context():
result, error = safe_db_operation(
lambda: (_ for _ in ()).throw(RuntimeError("db error")),
"Operation failed",
)
assert result is None
assert error.status_code == 400
assert error.json["message"] == "Operation failed"
def test_hides_exception_details(self, app):
from application.api.user.utils import safe_db_operation
with app.app_context():
_, error = safe_db_operation(
lambda: (_ for _ in ()).throw(RuntimeError("secret credentials")),
"Failed",
)
assert "credentials" not in error.json["message"]
@pytest.mark.unit
class TestValidateEnum:
def test_valid_value(self, app):
from application.api.user.utils import validate_enum
with app.app_context():
assert validate_enum("draft", ["draft", "published"], "status") is None
def test_invalid_value(self, app):
from application.api.user.utils import validate_enum
with app.app_context():
error = validate_enum("unknown", ["draft", "published"], "status")
assert error.status_code == 400
assert "status" in error.json["message"]
@pytest.mark.unit
class TestExtractSortParams:
def test_defaults(self, app):
from application.api.user.utils import extract_sort_params
with app.test_request_context("/"):
field, order = extract_sort_params()
assert field == "created_at"
assert order == -1
def test_custom_params(self, app):
from application.api.user.utils import extract_sort_params
with app.test_request_context("/?sort=name&order=asc"):
field, order = extract_sort_params()
assert field == "name"
assert order == 1
def test_enforces_allowed_fields(self, app):
from application.api.user.utils import extract_sort_params
with app.test_request_context("/?sort=forbidden_field"):
field, _ = extract_sort_params(allowed_fields=["name", "date"])
assert field == "created_at"
def test_desc_order(self, app):
from application.api.user.utils import extract_sort_params
with app.test_request_context("/?order=desc"):
_, order = extract_sort_params()
assert order == -1
+225
View File
@@ -0,0 +1,225 @@
from unittest.mock import Mock, patch
import pytest
from bson import ObjectId
from flask import Flask
@pytest.fixture
def app():
app = Flask(__name__)
return app
@pytest.mark.unit
class TestAgentWebhook:
def test_returns_existing_webhook_url(self, app):
from application.api.user.agents.webhooks import AgentWebhook
agent_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": agent_id,
"user": "user1",
"incoming_webhook_token": "existing_token",
}
with patch(
"application.api.user.agents.webhooks.agents_collection",
mock_collection,
), patch(
"application.api.user.agents.webhooks.settings",
Mock(API_URL="https://api.example.com"),
):
with app.test_request_context(
f"/api/agent_webhook?id={agent_id}"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentWebhook().get()
assert response.status_code == 200
assert response.json["success"] is True
assert "existing_token" in response.json["webhook_url"]
mock_collection.update_one.assert_not_called()
def test_generates_new_webhook_token(self, app):
from application.api.user.agents.webhooks import AgentWebhook
agent_id = ObjectId()
mock_collection = Mock()
mock_collection.find_one.return_value = {
"_id": agent_id,
"user": "user1",
"incoming_webhook_token": None,
}
with patch(
"application.api.user.agents.webhooks.agents_collection",
mock_collection,
), patch(
"application.api.user.agents.webhooks.settings",
Mock(API_URL="https://api.example.com"),
), patch(
"application.api.user.agents.webhooks.secrets.token_urlsafe",
return_value="new_generated_token",
):
with app.test_request_context(
f"/api/agent_webhook?id={agent_id}"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentWebhook().get()
assert response.status_code == 200
assert "new_generated_token" in response.json["webhook_url"]
mock_collection.update_one.assert_called_once()
def test_returns_401_unauthenticated(self, app):
from application.api.user.agents.webhooks import AgentWebhook
with app.test_request_context(f"/api/agent_webhook?id={ObjectId()}"):
from flask import request
request.decoded_token = None
response = AgentWebhook().get()
assert response.status_code == 401
def test_returns_400_missing_id(self, app):
from application.api.user.agents.webhooks import AgentWebhook
with app.test_request_context("/api/agent_webhook"):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentWebhook().get()
assert response.status_code == 400
def test_returns_404_agent_not_found(self, app):
from application.api.user.agents.webhooks import AgentWebhook
mock_collection = Mock()
mock_collection.find_one.return_value = None
with patch(
"application.api.user.agents.webhooks.agents_collection",
mock_collection,
):
with app.test_request_context(
f"/api/agent_webhook?id={ObjectId()}"
):
from flask import request
request.decoded_token = {"sub": "user1"}
response = AgentWebhook().get()
assert response.status_code == 404
@pytest.mark.unit
class TestAgentWebhookListenerPost:
def test_enqueues_task_on_valid_post(self, app):
from application.api.user.agents.webhooks import AgentWebhookListener
mock_task = Mock()
mock_task.id = "task_abc"
with patch(
"application.api.user.agents.webhooks.process_agent_webhook"
) as mock_process:
mock_process.delay.return_value = mock_task
with app.test_request_context(
"/api/webhooks/agents/tok",
method="POST",
json={"event": "new_message"},
):
listener = AgentWebhookListener()
response = listener._enqueue_webhook_task(
"agent123", {"event": "new_message"}, "POST"
)
assert response.status_code == 200
assert response.json["task_id"] == "task_abc"
mock_process.delay.assert_called_once_with(
agent_id="agent123", payload={"event": "new_message"}
)
def test_returns_400_on_missing_json(self, app):
from application.api.user.agents.webhooks import AgentWebhookListener
with app.test_request_context(
"/api/webhooks/agents/tok",
method="POST",
json=None,
content_type="application/json",
data="",
):
from flask import request as flask_request
# Force get_json to return None (simulating empty/missing body)
with patch.object(
flask_request, "get_json", return_value=None
):
listener = AgentWebhookListener()
response = listener.post(
webhook_token="tok",
agent={"_id": ObjectId()},
agent_id_str="agent123",
)
assert response.status_code == 400
def test_handles_enqueue_error(self, app):
from application.api.user.agents.webhooks import AgentWebhookListener
with patch(
"application.api.user.agents.webhooks.process_agent_webhook"
) as mock_process:
mock_process.delay.side_effect = Exception("Queue down")
with app.test_request_context(
"/api/webhooks/agents/tok",
method="POST",
json={"event": "test"},
):
listener = AgentWebhookListener()
response = listener._enqueue_webhook_task(
"agent123", {"event": "test"}, "POST"
)
assert response.status_code == 500
@pytest.mark.unit
class TestAgentWebhookListenerGet:
def test_uses_query_params_as_payload(self, app):
from application.api.user.agents.webhooks import AgentWebhookListener
mock_task = Mock()
mock_task.id = "task_xyz"
with patch(
"application.api.user.agents.webhooks.process_agent_webhook"
) as mock_process:
mock_process.delay.return_value = mock_task
with app.test_request_context(
"/api/webhooks/agents/tok?event=ping&source=test",
method="GET",
):
listener = AgentWebhookListener()
response = listener.get(
webhook_token="tok",
agent={"_id": ObjectId()},
agent_id_str="agent456",
)
assert response.status_code == 200
call_kwargs = mock_process.delay.call_args[1]
assert call_kwargs["payload"]["event"] == "ping"
assert call_kwargs["payload"]["source"] == "test"
+406
View File
@@ -0,0 +1,406 @@
from datetime import datetime, timezone
from unittest.mock import Mock, patch
import pytest
from bson import ObjectId
@pytest.mark.unit
class TestSerializeWorkflow:
def test_serializes_full_workflow(self):
from application.api.user.workflows.routes import serialize_workflow
now = datetime(2024, 6, 15, 10, 30, 0, tzinfo=timezone.utc)
doc = {
"_id": ObjectId(),
"name": "My Workflow",
"description": "A test workflow",
"created_at": now,
"updated_at": now,
}
result = serialize_workflow(doc)
assert result["id"] == str(doc["_id"])
assert result["name"] == "My Workflow"
assert result["description"] == "A test workflow"
assert result["created_at"] == now.isoformat()
def test_handles_missing_optional_fields(self):
from application.api.user.workflows.routes import serialize_workflow
doc = {"_id": ObjectId()}
result = serialize_workflow(doc)
assert result["name"] is None
assert result["created_at"] is None
@pytest.mark.unit
class TestSerializeNode:
def test_serializes_node(self):
from application.api.user.workflows.routes import serialize_node
node = {
"id": "node-1",
"type": "agent",
"title": "Agent Node",
"description": "Does things",
"position": {"x": 100, "y": 200},
"config": {"model": "gpt-4"},
}
result = serialize_node(node)
assert result["id"] == "node-1"
assert result["type"] == "agent"
assert result["title"] == "Agent Node"
assert result["data"] == {"model": "gpt-4"}
assert result["position"] == {"x": 100, "y": 200}
def test_defaults_for_missing_fields(self):
from application.api.user.workflows.routes import serialize_node
node = {"id": "n1", "type": "start"}
result = serialize_node(node)
assert result["data"] == {}
assert result["title"] is None
@pytest.mark.unit
class TestSerializeEdge:
def test_serializes_edge(self):
from application.api.user.workflows.routes import serialize_edge
edge = {
"id": "edge-1",
"source_id": "node-1",
"target_id": "node-2",
"source_handle": "output",
"target_handle": "input",
}
result = serialize_edge(edge)
assert result["id"] == "edge-1"
assert result["source"] == "node-1"
assert result["target"] == "node-2"
assert result["sourceHandle"] == "output"
assert result["targetHandle"] == "input"
@pytest.mark.unit
class TestGetWorkflowGraphVersion:
def test_returns_version(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": 3}) == 3
def test_defaults_to_1(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({}) == 1
def test_handles_invalid_version(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": "bad"}) == 1
def test_handles_zero_version(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": 0}) == 1
def test_handles_negative_version(self):
from application.api.user.workflows.routes import get_workflow_graph_version
assert get_workflow_graph_version({"current_graph_version": -1}) == 1
@pytest.mark.unit
class TestFetchGraphDocuments:
def test_returns_versioned_docs(self):
from application.api.user.workflows.routes import fetch_graph_documents
collection = Mock()
docs = [{"id": "n1", "graph_version": 2}]
collection.find.return_value = docs
result = fetch_graph_documents(collection, "wf1", 2)
assert result == docs
collection.find.assert_called_once_with(
{"workflow_id": "wf1", "graph_version": 2}
)
def test_falls_back_to_unversioned_for_v1(self):
from application.api.user.workflows.routes import fetch_graph_documents
collection = Mock()
unversioned_docs = [{"id": "n1"}]
collection.find.side_effect = [[], unversioned_docs]
result = fetch_graph_documents(collection, "wf1", 1)
assert result == unversioned_docs
assert collection.find.call_count == 2
def test_no_fallback_for_higher_versions(self):
from application.api.user.workflows.routes import fetch_graph_documents
collection = Mock()
collection.find.return_value = []
result = fetch_graph_documents(collection, "wf1", 3)
assert result == []
assert collection.find.call_count == 1
@pytest.mark.unit
class TestValidateWorkflowStructure:
def _make_minimal_workflow(self):
nodes = [
{"id": "start", "type": "start"},
{"id": "end", "type": "end"},
]
edges = [{"id": "e1", "source": "start", "target": "end"}]
return nodes, edges
def test_valid_minimal_workflow(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes, edges = self._make_minimal_workflow()
errors = validate_workflow_structure(nodes, edges)
assert errors == []
def test_empty_nodes(self):
from application.api.user.workflows.routes import validate_workflow_structure
errors = validate_workflow_structure([], [])
assert any("at least one node" in e for e in errors)
def test_missing_start_node(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [{"id": "end", "type": "end"}]
edges = []
errors = validate_workflow_structure(nodes, edges)
assert any("start node" in e for e in errors)
def test_missing_end_node(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [{"id": "start", "type": "start"}]
edges = [{"id": "e1", "source": "start", "target": "somewhere"}]
errors = validate_workflow_structure(nodes, edges)
assert any("end node" in e for e in errors)
def test_start_node_without_outgoing_edge(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{"id": "end", "type": "end"},
]
edges = []
errors = validate_workflow_structure(nodes, edges)
assert any("outgoing edge" in e for e in errors)
def test_edge_references_nonexistent_node(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{"id": "end", "type": "end"},
]
edges = [{"id": "e1", "source": "start", "target": "ghost"}]
errors = validate_workflow_structure(nodes, edges)
assert any("non-existent target" in e for e in errors)
def test_node_without_id(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{"type": "end"},
]
edges = [{"id": "e1", "source": "start", "target": None}]
errors = validate_workflow_structure(nodes, edges)
assert any("must have an id" in e for e in errors)
def test_node_without_type(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{"id": "end"},
]
edges = [{"id": "e1", "source": "start", "target": "end"}]
errors = validate_workflow_structure(nodes, edges)
assert any("must have a type" in e for e in errors)
def test_condition_node_needs_two_outgoing_edges(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{
"id": "cond",
"type": "condition",
"title": "Check",
"data": {
"cases": [
{"expression": "x > 1", "sourceHandle": "case1"},
]
},
},
{"id": "end", "type": "end"},
]
edges = [
{"id": "e1", "source": "start", "target": "cond"},
{"id": "e2", "source": "cond", "target": "end", "sourceHandle": "else"},
]
errors = validate_workflow_structure(nodes, edges)
assert any("at least 2 outgoing edges" in e for e in errors)
def test_condition_node_needs_else_branch(self):
from application.api.user.workflows.routes import validate_workflow_structure
nodes = [
{"id": "start", "type": "start"},
{
"id": "cond",
"type": "condition",
"title": "Check",
"data": {
"cases": [
{"expression": "x > 1", "sourceHandle": "case1"},
]
},
},
{"id": "end1", "type": "end"},
{"id": "end2", "type": "end"},
]
edges = [
{"id": "e1", "source": "start", "target": "cond"},
{"id": "e2", "source": "cond", "target": "end1", "sourceHandle": "case1"},
{"id": "e3", "source": "cond", "target": "end2", "sourceHandle": "case1"},
]
errors = validate_workflow_structure(nodes, edges)
assert any("else" in e for e in errors)
@pytest.mark.unit
class TestCanReachEnd:
def test_direct_end_node(self):
from application.api.user.workflows.routes import _can_reach_end
node_map = {"end": {"id": "end", "type": "end"}}
assert _can_reach_end("end", [], node_map, {"end"}) is True
def test_reachable_through_chain(self):
from application.api.user.workflows.routes import _can_reach_end
node_map = {
"a": {"id": "a"},
"b": {"id": "b"},
"end": {"id": "end", "type": "end"},
}
edges = [
{"source": "a", "target": "b"},
{"source": "b", "target": "end"},
]
assert _can_reach_end("a", edges, node_map, {"end"}) is True
def test_unreachable(self):
from application.api.user.workflows.routes import _can_reach_end
node_map = {
"a": {"id": "a"},
"b": {"id": "b"},
}
edges = [{"source": "a", "target": "b"}]
assert _can_reach_end("a", edges, node_map, {"end"}) is False
def test_handles_cycles(self):
from application.api.user.workflows.routes import _can_reach_end
node_map = {"a": {"id": "a"}, "b": {"id": "b"}}
edges = [
{"source": "a", "target": "b"},
{"source": "b", "target": "a"},
]
assert _can_reach_end("a", edges, node_map, {"end"}) is False
@pytest.mark.unit
class TestValidateJsonSchemaPayload:
def test_none_input(self):
from application.api.user.workflows.routes import validate_json_schema_payload
result, error = validate_json_schema_payload(None)
assert result is None
assert error is None
@patch("application.api.user.workflows.routes.normalize_json_schema_payload")
def test_valid_schema(self, mock_normalize):
from application.api.user.workflows.routes import validate_json_schema_payload
mock_normalize.return_value = {"type": "object"}
result, error = validate_json_schema_payload({"type": "object"})
assert result == {"type": "object"}
assert error is None
@patch("application.api.user.workflows.routes.normalize_json_schema_payload")
def test_invalid_schema(self, mock_normalize):
from application.api.user.workflows.routes import validate_json_schema_payload
from application.core.json_schema_utils import JsonSchemaValidationError
mock_normalize.side_effect = JsonSchemaValidationError("bad schema")
result, error = validate_json_schema_payload({"bad": True})
assert result is None
assert "bad schema" in error
@pytest.mark.unit
class TestNormalizeAgentNodeJsonSchemas:
def test_non_agent_nodes_pass_through(self):
from application.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
nodes = [
{"id": "n1", "type": "start"},
{"id": "n2", "type": "end"},
]
result = normalize_agent_node_json_schemas(nodes)
assert result == nodes
@patch("application.api.user.workflows.routes.normalize_json_schema_payload")
def test_normalizes_agent_node_schema(self, mock_normalize):
from application.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
mock_normalize.return_value = {"type": "object", "properties": {}}
nodes = [
{
"id": "a1",
"type": "agent",
"data": {"json_schema": {"type": "object"}},
}
]
result = normalize_agent_node_json_schemas(nodes)
assert result[0]["data"]["json_schema"] == {
"type": "object",
"properties": {},
}
def test_agent_node_without_schema(self):
from application.api.user.workflows.routes import (
normalize_agent_node_json_schemas,
)
nodes = [{"id": "a1", "type": "agent", "data": {"model": "gpt-4"}}]
result = normalize_agent_node_json_schemas(nodes)
assert result[0]["data"] == {"model": "gpt-4"}