atlas-chat 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the env-demo MCP server.
|
|
3
|
+
Tests the environment variable demonstration functionality.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Test that the server can be imported
|
|
11
|
+
def test_server_imports():
|
|
12
|
+
"""Test that the env-demo server module can be imported."""
|
|
13
|
+
try:
|
|
14
|
+
import sys
|
|
15
|
+
sys.path.insert(0, '/home/runner/work/atlas-ui-3/atlas-ui-3/backend')
|
|
16
|
+
# Import the module by file path since directory has hyphen
|
|
17
|
+
import importlib.util
|
|
18
|
+
spec = importlib.util.spec_from_file_location(
|
|
19
|
+
"env_demo_main",
|
|
20
|
+
"/home/runner/work/atlas-ui-3/atlas-ui-3/backend/mcp/env-demo/main.py"
|
|
21
|
+
)
|
|
22
|
+
module = importlib.util.module_from_spec(spec)
|
|
23
|
+
# Don't execute - just verify it can be loaded
|
|
24
|
+
assert spec is not None
|
|
25
|
+
assert module is not None
|
|
26
|
+
except Exception as e:
|
|
27
|
+
pytest.fail(f"Failed to import env-demo server: {e}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_env_var_configuration():
|
|
31
|
+
"""Test that environment variables are accessible."""
|
|
32
|
+
# Set test environment variables
|
|
33
|
+
os.environ["TEST_CLOUD_PROFILE"] = "test-profile"
|
|
34
|
+
os.environ["TEST_CLOUD_REGION"] = "test-region"
|
|
35
|
+
|
|
36
|
+
# Verify they can be read
|
|
37
|
+
assert os.environ.get("TEST_CLOUD_PROFILE") == "test-profile"
|
|
38
|
+
assert os.environ.get("TEST_CLOUD_REGION") == "test-region"
|
|
39
|
+
|
|
40
|
+
# Clean up
|
|
41
|
+
del os.environ["TEST_CLOUD_PROFILE"]
|
|
42
|
+
del os.environ["TEST_CLOUD_REGION"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_env_var_substitution_pattern():
|
|
46
|
+
"""Test the ${VAR} pattern that should be resolved by config_manager."""
|
|
47
|
+
# This tests the pattern that config_manager.resolve_env_var handles
|
|
48
|
+
# We test the pattern matching logic directly
|
|
49
|
+
import re
|
|
50
|
+
|
|
51
|
+
# Set a test variable
|
|
52
|
+
os.environ["TEST_API_KEY"] = "secret-123"
|
|
53
|
+
|
|
54
|
+
# Test the ${VAR} pattern matching (same as config_manager.resolve_env_var)
|
|
55
|
+
pattern = r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}'
|
|
56
|
+
|
|
57
|
+
# Test resolution
|
|
58
|
+
test_value = "${TEST_API_KEY}"
|
|
59
|
+
match = re.match(pattern, test_value)
|
|
60
|
+
assert match is not None
|
|
61
|
+
var_name = match.group(1)
|
|
62
|
+
assert var_name == "TEST_API_KEY"
|
|
63
|
+
resolved = os.environ.get(var_name)
|
|
64
|
+
assert resolved == "secret-123"
|
|
65
|
+
|
|
66
|
+
# Test literal value (no substitution)
|
|
67
|
+
test_value = "literal-value"
|
|
68
|
+
match = re.match(pattern, test_value)
|
|
69
|
+
assert match is None # Should not match
|
|
70
|
+
|
|
71
|
+
# Test missing variable
|
|
72
|
+
test_value = "${MISSING_VAR}"
|
|
73
|
+
match = re.match(pattern, test_value)
|
|
74
|
+
assert match is not None
|
|
75
|
+
var_name = match.group(1)
|
|
76
|
+
assert var_name == "MISSING_VAR"
|
|
77
|
+
missing_var = os.environ.get(var_name)
|
|
78
|
+
assert missing_var is None # Variable doesn't exist
|
|
79
|
+
|
|
80
|
+
# Clean up
|
|
81
|
+
del os.environ["TEST_API_KEY"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Tests for error classification and user-friendly error messages."""
|
|
2
|
+
|
|
3
|
+
from atlas.application.chat.utilities.error_handler import classify_llm_error
|
|
4
|
+
from atlas.domain.errors import LLMAuthenticationError, LLMServiceError, LLMTimeoutError, RateLimitError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestErrorClassification:
|
|
8
|
+
"""Test error classification for LLM errors."""
|
|
9
|
+
|
|
10
|
+
def test_classify_rate_limit_error_by_type_name(self):
|
|
11
|
+
"""Test classification of rate limit errors by exception type name."""
|
|
12
|
+
# Create a custom exception class to test type name detection
|
|
13
|
+
class RateLimitError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
error = RateLimitError("Some error message")
|
|
17
|
+
|
|
18
|
+
from atlas.domain.errors import RateLimitError as DomainRateLimitError
|
|
19
|
+
error_class, user_msg, log_msg = classify_llm_error(error)
|
|
20
|
+
|
|
21
|
+
assert error_class == DomainRateLimitError
|
|
22
|
+
assert "high traffic" in user_msg.lower()
|
|
23
|
+
assert "try again" in user_msg.lower()
|
|
24
|
+
assert "rate limit" in log_msg.lower()
|
|
25
|
+
|
|
26
|
+
def test_classify_rate_limit_error_by_message_content(self):
|
|
27
|
+
"""Test classification of rate limit errors by message content."""
|
|
28
|
+
error = Exception("We're experiencing high traffic right now! Please try again soon.")
|
|
29
|
+
|
|
30
|
+
error_class, user_msg, log_msg = classify_llm_error(error)
|
|
31
|
+
|
|
32
|
+
assert error_class == RateLimitError
|
|
33
|
+
assert "high traffic" in user_msg.lower()
|
|
34
|
+
assert "try again" in user_msg.lower()
|
|
35
|
+
|
|
36
|
+
def test_classify_rate_limit_error_alternative_message(self):
|
|
37
|
+
"""Test classification of rate limit errors with alternative wording."""
|
|
38
|
+
error = Exception("Rate limit exceeded for this API key")
|
|
39
|
+
|
|
40
|
+
error_class, user_msg, log_msg = classify_llm_error(error)
|
|
41
|
+
|
|
42
|
+
assert error_class == RateLimitError
|
|
43
|
+
assert "try again" in user_msg.lower()
|
|
44
|
+
|
|
45
|
+
def test_classify_timeout_error(self):
|
|
46
|
+
"""Test classification of timeout errors."""
|
|
47
|
+
error = Exception("Request timed out after 30 seconds")
|
|
48
|
+
|
|
49
|
+
error_class, user_msg, log_msg = classify_llm_error(error)
|
|
50
|
+
|
|
51
|
+
assert error_class == LLMTimeoutError
|
|
52
|
+
assert "timeout" in user_msg.lower() or "timed out" in user_msg.lower()
|
|
53
|
+
assert "try again" in user_msg.lower()
|
|
54
|
+
|
|
55
|
+
def test_classify_authentication_error(self):
|
|
56
|
+
"""Test classification of authentication errors."""
|
|
57
|
+
error = Exception("Invalid API key provided")
|
|
58
|
+
|
|
59
|
+
error_class, user_msg, log_msg = classify_llm_error(error)
|
|
60
|
+
|
|
61
|
+
assert error_class == LLMAuthenticationError
|
|
62
|
+
assert "authentication" in user_msg.lower()
|
|
63
|
+
assert "administrator" in user_msg.lower()
|
|
64
|
+
|
|
65
|
+
def test_classify_unauthorized_error(self):
|
|
66
|
+
"""Test classification of unauthorized errors."""
|
|
67
|
+
error = Exception("Unauthorized access")
|
|
68
|
+
|
|
69
|
+
error_class, user_msg, log_msg = classify_llm_error(error)
|
|
70
|
+
|
|
71
|
+
assert error_class == LLMAuthenticationError
|
|
72
|
+
assert "authentication" in user_msg.lower()
|
|
73
|
+
|
|
74
|
+
def test_classify_generic_llm_error(self):
|
|
75
|
+
"""Test classification of generic LLM errors."""
|
|
76
|
+
error = Exception("Something went wrong with the model")
|
|
77
|
+
|
|
78
|
+
error_class, user_msg, log_msg = classify_llm_error(error)
|
|
79
|
+
|
|
80
|
+
assert error_class == LLMServiceError
|
|
81
|
+
assert "error" in user_msg.lower()
|
|
82
|
+
assert "try again" in user_msg.lower() or "contact support" in user_msg.lower()
|
|
83
|
+
|
|
84
|
+
def test_error_messages_are_user_friendly(self):
|
|
85
|
+
"""Test that all error messages are user-friendly (no technical details)."""
|
|
86
|
+
test_errors = [
|
|
87
|
+
Exception("RateLimitError: Rate limit exceeded"),
|
|
88
|
+
Exception("Request timeout after 60s"),
|
|
89
|
+
Exception("Invalid API key: abc123"),
|
|
90
|
+
Exception("Unknown model error"),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
for error in test_errors:
|
|
94
|
+
_, user_msg, _ = classify_llm_error(error)
|
|
95
|
+
|
|
96
|
+
# User messages should be helpful and not expose technical details
|
|
97
|
+
assert len(user_msg) > 20 # Should be a complete sentence
|
|
98
|
+
# Technical details should not appear in user message
|
|
99
|
+
technical_substrings = ["RateLimitError:", "abc123", "stack trace"]
|
|
100
|
+
for technical in technical_substrings:
|
|
101
|
+
assert technical not in user_msg, f"User message should not contain technical detail: {technical}"
|
|
102
|
+
assert user_msg[0].isupper() # Starts with capital letter
|
|
103
|
+
assert user_msg.endswith(".") # Ends with period
|
|
104
|
+
|
|
105
|
+
def test_log_messages_contain_error_details(self):
|
|
106
|
+
"""Test that log messages contain error details for debugging."""
|
|
107
|
+
error = Exception("RateLimitError: We're experiencing high traffic")
|
|
108
|
+
|
|
109
|
+
_, _, log_msg = classify_llm_error(error)
|
|
110
|
+
|
|
111
|
+
# Log message should contain the actual error for debugging
|
|
112
|
+
assert "high traffic" in log_msg.lower()
|
|
113
|
+
assert len(log_msg) > 10
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Integration test for error flow from LLM to WebSocket."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from atlas.domain.errors import LLMAuthenticationError, LLMTimeoutError, RateLimitError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestErrorFlowIntegration:
|
|
11
|
+
"""Test that errors flow correctly from LLM through to error responses."""
|
|
12
|
+
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
async def test_rate_limit_error_flow(self):
|
|
15
|
+
"""Test that rate limit errors result in proper user-friendly messages."""
|
|
16
|
+
from atlas.application.chat.utilities.error_handler import safe_call_llm_with_tools
|
|
17
|
+
|
|
18
|
+
# Mock LLM caller that raises a rate limit error
|
|
19
|
+
mock_llm = MagicMock()
|
|
20
|
+
mock_llm.call_with_tools = AsyncMock(
|
|
21
|
+
side_effect=Exception("RateLimitError: We're experiencing high traffic right now! Please try again soon.")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Call should raise our custom RateLimitError
|
|
25
|
+
with pytest.raises(RateLimitError) as exc_info:
|
|
26
|
+
await safe_call_llm_with_tools(
|
|
27
|
+
llm_caller=mock_llm,
|
|
28
|
+
model="test-model",
|
|
29
|
+
messages=[{"role": "user", "content": "test"}],
|
|
30
|
+
tools_schema=[],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Verify the error message is user-friendly
|
|
34
|
+
error_msg = str(exc_info.value.message if hasattr(exc_info.value, 'message') else exc_info.value)
|
|
35
|
+
assert "high traffic" in error_msg.lower()
|
|
36
|
+
assert "try again" in error_msg.lower()
|
|
37
|
+
# Should NOT contain technical details
|
|
38
|
+
assert "RateLimitError:" not in error_msg
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_timeout_error_flow(self):
|
|
42
|
+
"""Test that timeout errors result in proper user-friendly messages."""
|
|
43
|
+
from atlas.application.chat.utilities.error_handler import safe_call_llm_with_tools
|
|
44
|
+
|
|
45
|
+
# Mock LLM caller that raises a timeout error
|
|
46
|
+
mock_llm = MagicMock()
|
|
47
|
+
mock_llm.call_with_tools = AsyncMock(
|
|
48
|
+
side_effect=Exception("Request timed out after 60 seconds")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Call should raise our custom LLMTimeoutError
|
|
52
|
+
with pytest.raises(LLMTimeoutError) as exc_info:
|
|
53
|
+
await safe_call_llm_with_tools(
|
|
54
|
+
llm_caller=mock_llm,
|
|
55
|
+
model="test-model",
|
|
56
|
+
messages=[{"role": "user", "content": "test"}],
|
|
57
|
+
tools_schema=[],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Verify the error message is user-friendly
|
|
61
|
+
error_msg = str(exc_info.value.message if hasattr(exc_info.value, 'message') else exc_info.value)
|
|
62
|
+
assert "timeout" in error_msg.lower() or "timed out" in error_msg.lower()
|
|
63
|
+
assert "try again" in error_msg.lower()
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_authentication_error_flow(self):
|
|
67
|
+
"""Test that authentication errors result in proper user-friendly messages."""
|
|
68
|
+
from atlas.application.chat.utilities.error_handler import safe_call_llm_with_tools
|
|
69
|
+
|
|
70
|
+
# Mock LLM caller that raises an auth error
|
|
71
|
+
mock_llm = MagicMock()
|
|
72
|
+
mock_llm.call_with_tools = AsyncMock(
|
|
73
|
+
side_effect=Exception("Invalid API key provided")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Call should raise our custom LLMAuthenticationError
|
|
77
|
+
with pytest.raises(LLMAuthenticationError) as exc_info:
|
|
78
|
+
await safe_call_llm_with_tools(
|
|
79
|
+
llm_caller=mock_llm,
|
|
80
|
+
model="test-model",
|
|
81
|
+
messages=[{"role": "user", "content": "test"}],
|
|
82
|
+
tools_schema=[],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Verify the error message is user-friendly
|
|
86
|
+
error_msg = str(exc_info.value.message if hasattr(exc_info.value, 'message') else exc_info.value)
|
|
87
|
+
assert "authentication" in error_msg.lower()
|
|
88
|
+
assert "administrator" in error_msg.lower()
|
|
89
|
+
# Should NOT contain the actual API key reference
|
|
90
|
+
assert "API key" not in error_msg and "api key" not in error_msg.lower()
|
|
91
|
+
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_successful_llm_call(self):
|
|
94
|
+
"""Test that successful LLM calls work normally."""
|
|
95
|
+
from atlas.application.chat.utilities.error_handler import safe_call_llm_with_tools
|
|
96
|
+
from atlas.interfaces.llm import LLMResponse
|
|
97
|
+
|
|
98
|
+
# Mock successful LLM response
|
|
99
|
+
mock_response = LLMResponse(
|
|
100
|
+
content="Test response",
|
|
101
|
+
model_used="test-model"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
mock_llm = MagicMock()
|
|
105
|
+
mock_llm.call_with_tools = AsyncMock(return_value=mock_response)
|
|
106
|
+
|
|
107
|
+
# Call should succeed
|
|
108
|
+
result = await safe_call_llm_with_tools(
|
|
109
|
+
llm_caller=mock_llm,
|
|
110
|
+
model="test-model",
|
|
111
|
+
messages=[{"role": "user", "content": "test"}],
|
|
112
|
+
tools_schema=[],
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert result == mock_response
|
|
116
|
+
assert result.content == "Test response"
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Tests for feedback routes to prevent regression of issue #200.
|
|
2
|
+
|
|
3
|
+
Verifies that:
|
|
4
|
+
- POST /api/feedback is accessible to authenticated users
|
|
5
|
+
- GET /api/feedback requires admin group membership
|
|
6
|
+
- GET /api/feedback/stats requires admin group membership
|
|
7
|
+
- DELETE /api/feedback/{id} requires admin group membership
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from unittest.mock import patch
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
from main import app
|
|
17
|
+
from starlette.testclient import TestClient
|
|
18
|
+
|
|
19
|
+
AUTH_HEADERS = {"X-User-Email": "test@test.com"}
|
|
20
|
+
ADMIN_HEADERS = {"X-User-Email": "admin@test.com"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def temp_feedback_dir():
|
|
25
|
+
"""Create a temporary directory for feedback files."""
|
|
26
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
27
|
+
yield Path(tmpdir)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def mock_feedback_dir(temp_feedback_dir, monkeypatch):
|
|
32
|
+
"""Mock the feedback directory to use temp directory."""
|
|
33
|
+
def mock_get_feedback_directory():
|
|
34
|
+
return temp_feedback_dir
|
|
35
|
+
|
|
36
|
+
monkeypatch.setattr(
|
|
37
|
+
"atlas.routes.feedback_routes.get_feedback_directory",
|
|
38
|
+
mock_get_feedback_directory
|
|
39
|
+
)
|
|
40
|
+
return temp_feedback_dir
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def mock_admin_check():
|
|
45
|
+
"""Mock admin group check to allow admin@test.com."""
|
|
46
|
+
async def mock_is_user_in_group(user: str, group: str) -> bool:
|
|
47
|
+
return user == "admin@test.com"
|
|
48
|
+
|
|
49
|
+
with patch("atlas.routes.feedback_routes.is_user_in_group", mock_is_user_in_group):
|
|
50
|
+
yield
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestFeedbackRouteRegistration:
|
|
54
|
+
"""Test that feedback routes are properly registered (issue #200)."""
|
|
55
|
+
|
|
56
|
+
def test_post_feedback_route_exists(self):
|
|
57
|
+
"""POST /api/feedback should not return 404."""
|
|
58
|
+
client = TestClient(app)
|
|
59
|
+
resp = client.post(
|
|
60
|
+
"/api/feedback",
|
|
61
|
+
json={"rating": 1, "comment": "test", "session": {}},
|
|
62
|
+
headers=AUTH_HEADERS
|
|
63
|
+
)
|
|
64
|
+
assert resp.status_code != 404, "Feedback route not registered (issue #200)"
|
|
65
|
+
|
|
66
|
+
def test_get_feedback_route_exists(self):
|
|
67
|
+
"""GET /api/feedback should not return 404."""
|
|
68
|
+
client = TestClient(app)
|
|
69
|
+
resp = client.get("/api/feedback", headers=AUTH_HEADERS)
|
|
70
|
+
assert resp.status_code != 404, "Feedback route not registered (issue #200)"
|
|
71
|
+
|
|
72
|
+
def test_get_feedback_stats_route_exists(self):
|
|
73
|
+
"""GET /api/feedback/stats should not return 404."""
|
|
74
|
+
client = TestClient(app)
|
|
75
|
+
resp = client.get("/api/feedback/stats", headers=AUTH_HEADERS)
|
|
76
|
+
assert resp.status_code != 404, "Feedback stats route not registered (issue #200)"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestFeedbackSubmission:
|
|
80
|
+
"""Test feedback submission by regular users."""
|
|
81
|
+
|
|
82
|
+
def test_submit_feedback_success(self, mock_feedback_dir, mock_admin_check):
|
|
83
|
+
"""Regular users can submit feedback."""
|
|
84
|
+
client = TestClient(app)
|
|
85
|
+
resp = client.post(
|
|
86
|
+
"/api/feedback",
|
|
87
|
+
json={"rating": 1, "comment": "Great service!", "session": {"model": "gpt-4"}},
|
|
88
|
+
headers=AUTH_HEADERS
|
|
89
|
+
)
|
|
90
|
+
assert resp.status_code == 200
|
|
91
|
+
data = resp.json()
|
|
92
|
+
assert data["message"] == "Feedback submitted successfully"
|
|
93
|
+
assert "feedback_id" in data
|
|
94
|
+
assert "timestamp" in data
|
|
95
|
+
|
|
96
|
+
def test_submit_feedback_validates_rating(self, mock_feedback_dir, mock_admin_check):
|
|
97
|
+
"""Feedback submission validates rating values."""
|
|
98
|
+
client = TestClient(app)
|
|
99
|
+
resp = client.post(
|
|
100
|
+
"/api/feedback",
|
|
101
|
+
json={"rating": 5, "comment": "Invalid rating"},
|
|
102
|
+
headers=AUTH_HEADERS
|
|
103
|
+
)
|
|
104
|
+
assert resp.status_code == 400
|
|
105
|
+
assert "Rating must be -1, 0, or 1" in resp.json()["detail"]
|
|
106
|
+
|
|
107
|
+
def test_submit_feedback_creates_file(self, mock_feedback_dir, mock_admin_check):
|
|
108
|
+
"""Feedback submission creates a JSON file."""
|
|
109
|
+
client = TestClient(app)
|
|
110
|
+
resp = client.post(
|
|
111
|
+
"/api/feedback",
|
|
112
|
+
json={"rating": 0, "comment": "Neutral feedback"},
|
|
113
|
+
headers=AUTH_HEADERS
|
|
114
|
+
)
|
|
115
|
+
assert resp.status_code == 200
|
|
116
|
+
|
|
117
|
+
feedback_files = list(mock_feedback_dir.glob("feedback_*.json"))
|
|
118
|
+
assert len(feedback_files) == 1
|
|
119
|
+
|
|
120
|
+
with open(feedback_files[0]) as f:
|
|
121
|
+
saved_data = json.load(f)
|
|
122
|
+
assert saved_data["rating"] == 0
|
|
123
|
+
assert saved_data["comment"] == "Neutral feedback"
|
|
124
|
+
assert saved_data["user"] == "test@test.com"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestFeedbackAdminAccess:
|
|
128
|
+
"""Test that viewing feedback requires admin access."""
|
|
129
|
+
|
|
130
|
+
def test_get_feedback_requires_admin(self, mock_feedback_dir, mock_admin_check):
|
|
131
|
+
"""GET /api/feedback returns 403 for non-admin users."""
|
|
132
|
+
client = TestClient(app)
|
|
133
|
+
resp = client.get("/api/feedback", headers=AUTH_HEADERS)
|
|
134
|
+
assert resp.status_code == 403
|
|
135
|
+
assert "Admin access required" in resp.json()["detail"]
|
|
136
|
+
|
|
137
|
+
def test_get_feedback_stats_requires_admin(self, mock_feedback_dir, mock_admin_check):
|
|
138
|
+
"""GET /api/feedback/stats returns 403 for non-admin users."""
|
|
139
|
+
client = TestClient(app)
|
|
140
|
+
resp = client.get("/api/feedback/stats", headers=AUTH_HEADERS)
|
|
141
|
+
assert resp.status_code == 403
|
|
142
|
+
assert "Admin access required" in resp.json()["detail"]
|
|
143
|
+
|
|
144
|
+
def test_admin_can_view_feedback(self, mock_feedback_dir, mock_admin_check):
|
|
145
|
+
"""Admin users can view feedback list."""
|
|
146
|
+
client = TestClient(app)
|
|
147
|
+
|
|
148
|
+
client.post(
|
|
149
|
+
"/api/feedback",
|
|
150
|
+
json={"rating": 1, "comment": "Test"},
|
|
151
|
+
headers=AUTH_HEADERS
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
resp = client.get("/api/feedback", headers=ADMIN_HEADERS)
|
|
155
|
+
assert resp.status_code == 200
|
|
156
|
+
data = resp.json()
|
|
157
|
+
assert "feedback" in data
|
|
158
|
+
assert "pagination" in data
|
|
159
|
+
assert "statistics" in data
|
|
160
|
+
|
|
161
|
+
def test_admin_can_view_stats(self, mock_feedback_dir, mock_admin_check):
|
|
162
|
+
"""Admin users can view feedback statistics."""
|
|
163
|
+
client = TestClient(app)
|
|
164
|
+
resp = client.get("/api/feedback/stats", headers=ADMIN_HEADERS)
|
|
165
|
+
assert resp.status_code == 200
|
|
166
|
+
data = resp.json()
|
|
167
|
+
assert "total_feedback" in data
|
|
168
|
+
assert "rating_distribution" in data
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestFeedbackDeletion:
|
|
172
|
+
"""Test feedback deletion by admin."""
|
|
173
|
+
|
|
174
|
+
def test_delete_feedback_requires_admin(self, mock_feedback_dir, mock_admin_check):
|
|
175
|
+
"""DELETE /api/feedback/{id} returns 403 for non-admin users."""
|
|
176
|
+
client = TestClient(app)
|
|
177
|
+
resp = client.delete("/api/feedback/fake-id", headers=AUTH_HEADERS)
|
|
178
|
+
assert resp.status_code == 403
|
|
179
|
+
|
|
180
|
+
def test_admin_can_delete_feedback(self, mock_feedback_dir, mock_admin_check):
|
|
181
|
+
"""Admin users can delete feedback."""
|
|
182
|
+
client = TestClient(app)
|
|
183
|
+
|
|
184
|
+
resp = client.post(
|
|
185
|
+
"/api/feedback",
|
|
186
|
+
json={"rating": -1, "comment": "To be deleted"},
|
|
187
|
+
headers=AUTH_HEADERS
|
|
188
|
+
)
|
|
189
|
+
feedback_id = resp.json()["feedback_id"]
|
|
190
|
+
|
|
191
|
+
resp = client.delete(f"/api/feedback/{feedback_id}", headers=ADMIN_HEADERS)
|
|
192
|
+
assert resp.status_code == 200
|
|
193
|
+
assert resp.json()["message"] == "Feedback deleted successfully"
|
|
194
|
+
|
|
195
|
+
def test_delete_nonexistent_feedback_returns_404(self, mock_feedback_dir, mock_admin_check):
|
|
196
|
+
"""Deleting non-existent feedback returns 404."""
|
|
197
|
+
client = TestClient(app)
|
|
198
|
+
resp = client.delete("/api/feedback/nonexistent", headers=ADMIN_HEADERS)
|
|
199
|
+
assert resp.status_code == 404
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TestFeedbackDownload:
|
|
203
|
+
"""Test feedback download functionality."""
|
|
204
|
+
|
|
205
|
+
def test_download_feedback_requires_admin(self, mock_feedback_dir, mock_admin_check):
|
|
206
|
+
"""GET /api/feedback/download returns 403 for non-admin users."""
|
|
207
|
+
client = TestClient(app)
|
|
208
|
+
resp = client.get("/api/feedback/download", headers=AUTH_HEADERS)
|
|
209
|
+
assert resp.status_code == 403
|
|
210
|
+
|
|
211
|
+
def test_download_feedback_csv_format(self, mock_feedback_dir, mock_admin_check):
|
|
212
|
+
"""Admin users can download feedback as CSV."""
|
|
213
|
+
client = TestClient(app)
|
|
214
|
+
|
|
215
|
+
# Create some test feedback
|
|
216
|
+
client.post(
|
|
217
|
+
"/api/feedback",
|
|
218
|
+
json={"rating": 1, "comment": "Great service!", "session": {"model": "gpt-4"}},
|
|
219
|
+
headers=AUTH_HEADERS
|
|
220
|
+
)
|
|
221
|
+
client.post(
|
|
222
|
+
"/api/feedback",
|
|
223
|
+
json={"rating": -1, "comment": "Poor experience", "session": {"model": "gpt-3"}},
|
|
224
|
+
headers=AUTH_HEADERS
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Download as CSV
|
|
228
|
+
resp = client.get("/api/feedback/download?format=csv", headers=ADMIN_HEADERS)
|
|
229
|
+
assert resp.status_code == 200
|
|
230
|
+
assert resp.headers["content-type"] == "text/csv; charset=utf-8"
|
|
231
|
+
assert "attachment; filename=" in resp.headers["content-disposition"]
|
|
232
|
+
assert "feedback_export_" in resp.headers["content-disposition"]
|
|
233
|
+
|
|
234
|
+
# Verify CSV content
|
|
235
|
+
csv_content = resp.text
|
|
236
|
+
lines = [line.strip() for line in csv_content.strip().split('\n') if line.strip()]
|
|
237
|
+
assert len(lines) >= 3 # Header + 2 data rows
|
|
238
|
+
|
|
239
|
+
# Check header
|
|
240
|
+
assert lines[0] == "id,timestamp,user,rating,comment"
|
|
241
|
+
|
|
242
|
+
# Check that feedback appears in CSV (order may vary)
|
|
243
|
+
found_positive = any("1" in line and "Great service!" in line for line in lines[1:])
|
|
244
|
+
found_negative = any("-1" in line and "Poor experience" in line for line in lines[1:])
|
|
245
|
+
assert found_positive, "Positive feedback not found in CSV"
|
|
246
|
+
assert found_negative, "Negative feedback not found in CSV"
|
|
247
|
+
|
|
248
|
+
def test_download_feedback_json_format(self, mock_feedback_dir, mock_admin_check):
|
|
249
|
+
"""Admin users can download feedback as JSON."""
|
|
250
|
+
client = TestClient(app)
|
|
251
|
+
|
|
252
|
+
# Create test feedback
|
|
253
|
+
resp1 = client.post(
|
|
254
|
+
"/api/feedback",
|
|
255
|
+
json={"rating": 1, "comment": "JSON test", "session": {"model": "gpt-4"}},
|
|
256
|
+
headers=AUTH_HEADERS
|
|
257
|
+
)
|
|
258
|
+
feedback_id = resp1.json()["feedback_id"]
|
|
259
|
+
|
|
260
|
+
# Download as JSON
|
|
261
|
+
resp = client.get("/api/feedback/download?format=json", headers=ADMIN_HEADERS)
|
|
262
|
+
assert resp.status_code == 200
|
|
263
|
+
assert resp.headers["content-type"] == "application/json"
|
|
264
|
+
assert "attachment; filename=" in resp.headers["content-disposition"]
|
|
265
|
+
assert "feedback_export_" in resp.headers["content-disposition"]
|
|
266
|
+
|
|
267
|
+
# Verify JSON content
|
|
268
|
+
json_data = resp.json()
|
|
269
|
+
assert isinstance(json_data, list)
|
|
270
|
+
assert len(json_data) == 1
|
|
271
|
+
|
|
272
|
+
feedback = json_data[0]
|
|
273
|
+
assert feedback["id"] == feedback_id
|
|
274
|
+
assert feedback["rating"] == 1
|
|
275
|
+
assert feedback["comment"] == "JSON test"
|
|
276
|
+
assert feedback["user"] == "test@test.com"
|
|
277
|
+
assert "timestamp" in feedback
|
|
278
|
+
assert "session_info" in feedback
|
|
279
|
+
assert "server_context" in feedback
|
|
280
|
+
|
|
281
|
+
def test_download_feedback_empty_csv(self, mock_feedback_dir, mock_admin_check):
|
|
282
|
+
"""Downloading empty feedback as CSV returns header-only file."""
|
|
283
|
+
client = TestClient(app)
|
|
284
|
+
|
|
285
|
+
resp = client.get("/api/feedback/download?format=csv", headers=ADMIN_HEADERS)
|
|
286
|
+
assert resp.status_code == 200
|
|
287
|
+
|
|
288
|
+
csv_content = resp.text
|
|
289
|
+
lines = csv_content.strip().split('\n')
|
|
290
|
+
assert len(lines) == 1 # Only header row
|
|
291
|
+
assert lines[0] == "id,timestamp,user,rating,comment"
|
|
292
|
+
|
|
293
|
+
def test_download_feedback_empty_json(self, mock_feedback_dir, mock_admin_check):
|
|
294
|
+
"""Downloading empty feedback as JSON returns empty array."""
|
|
295
|
+
client = TestClient(app)
|
|
296
|
+
|
|
297
|
+
resp = client.get("/api/feedback/download?format=json", headers=ADMIN_HEADERS)
|
|
298
|
+
assert resp.status_code == 200
|
|
299
|
+
|
|
300
|
+
json_data = resp.json()
|
|
301
|
+
assert json_data == []
|
|
302
|
+
|
|
303
|
+
def test_download_feedback_csv_sanitizes_fields(self, mock_feedback_dir, mock_admin_check):
|
|
304
|
+
"""CSV download properly handles missing fields with defaults."""
|
|
305
|
+
client = TestClient(app)
|
|
306
|
+
|
|
307
|
+
# Clear any existing feedback files
|
|
308
|
+
for f in mock_feedback_dir.glob("feedback_*.json"):
|
|
309
|
+
f.unlink()
|
|
310
|
+
|
|
311
|
+
# Manually create feedback file with missing fields (using correct naming pattern)
|
|
312
|
+
import json
|
|
313
|
+
feedback_file = mock_feedback_dir / "feedback_manual_test123.json"
|
|
314
|
+
manual_feedback = {
|
|
315
|
+
"id": "test123",
|
|
316
|
+
"timestamp": "2026-01-10T12:00:00",
|
|
317
|
+
"user": "test@example.com"
|
|
318
|
+
# Missing rating, comment, session_info, server_context
|
|
319
|
+
}
|
|
320
|
+
with open(feedback_file, 'w') as f:
|
|
321
|
+
json.dump(manual_feedback, f)
|
|
322
|
+
|
|
323
|
+
resp = client.get("/api/feedback/download?format=csv", headers=ADMIN_HEADERS)
|
|
324
|
+
assert resp.status_code == 200
|
|
325
|
+
|
|
326
|
+
csv_content = resp.text
|
|
327
|
+
lines = csv_content.strip().split('\n')
|
|
328
|
+
assert len(lines) == 2 # Header + 1 data row
|
|
329
|
+
|
|
330
|
+
data_line = lines[1]
|
|
331
|
+
assert "test123" in data_line
|
|
332
|
+
assert "test@example.com" in data_line
|
|
333
|
+
# Missing fields should be empty strings
|