mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 20:15:30 +00:00
chore: more tests
This commit is contained in:
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
|
||||
@@ -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
|
||||
@@ -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 == []
|
||||
@@ -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"}'
|
||||
Whitespace-only changes.
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"}
|
||||
Reference in new issue
Block a user