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,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for BACKEND_PUBLIC_URL configuration and absolute URL generation.
|
|
3
|
+
|
|
4
|
+
This module tests that the file download URL generation correctly handles:
|
|
5
|
+
1. Relative URLs when BACKEND_PUBLIC_URL is not configured (backward compatibility)
|
|
6
|
+
2. Absolute URLs when BACKEND_PUBLIC_URL is configured (remote MCP server support)
|
|
7
|
+
3. Proper token generation and validation
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from unittest.mock import MagicMock, patch
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from atlas.core.capabilities import create_download_url, generate_file_token
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_mock_settings(backend_public_url=None):
|
|
18
|
+
"""Helper to create a properly mocked settings object."""
|
|
19
|
+
mock_settings = MagicMock()
|
|
20
|
+
mock_settings.backend_public_url = backend_public_url
|
|
21
|
+
mock_settings.capability_token_secret = "test-secret-key-for-testing"
|
|
22
|
+
mock_settings.capability_token_ttl_seconds = 3600
|
|
23
|
+
return mock_settings
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestBackendPublicUrlConfiguration:
|
|
27
|
+
"""Test suite for BACKEND_PUBLIC_URL configuration behavior."""
|
|
28
|
+
|
|
29
|
+
def test_relative_url_without_backend_public_url(self):
|
|
30
|
+
"""Test that relative URLs are generated when BACKEND_PUBLIC_URL is not set."""
|
|
31
|
+
mock_settings = create_mock_settings(backend_public_url=None)
|
|
32
|
+
|
|
33
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
34
|
+
mock_cm.app_settings = mock_settings
|
|
35
|
+
|
|
36
|
+
url = create_download_url("test-key-123", "user@example.com")
|
|
37
|
+
|
|
38
|
+
# Should start with / (relative URL)
|
|
39
|
+
assert url.startswith("/api/files/download/")
|
|
40
|
+
assert "test-key-123" in url
|
|
41
|
+
assert "token=" in url
|
|
42
|
+
assert not url.startswith("http")
|
|
43
|
+
|
|
44
|
+
def test_absolute_url_with_backend_public_url(self):
|
|
45
|
+
"""Test that absolute URLs are generated when BACKEND_PUBLIC_URL is configured."""
|
|
46
|
+
mock_settings = create_mock_settings(backend_public_url="https://atlas.example.com")
|
|
47
|
+
|
|
48
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
49
|
+
mock_cm.app_settings = mock_settings
|
|
50
|
+
|
|
51
|
+
url = create_download_url("test-key-456", "admin@example.com")
|
|
52
|
+
|
|
53
|
+
# Should be absolute URL
|
|
54
|
+
assert url.startswith("https://atlas.example.com/api/files/download/")
|
|
55
|
+
assert "test-key-456" in url
|
|
56
|
+
assert "token=" in url
|
|
57
|
+
|
|
58
|
+
def test_absolute_url_strips_trailing_slash(self):
|
|
59
|
+
"""Test that trailing slashes in BACKEND_PUBLIC_URL are handled correctly."""
|
|
60
|
+
mock_settings = create_mock_settings(backend_public_url="https://atlas.example.com/")
|
|
61
|
+
|
|
62
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
63
|
+
mock_cm.app_settings = mock_settings
|
|
64
|
+
|
|
65
|
+
url = create_download_url("test-key-789", "user@example.com")
|
|
66
|
+
|
|
67
|
+
# Should not have double slash
|
|
68
|
+
assert "https://atlas.example.com/api/files/download/" in url
|
|
69
|
+
assert "https://atlas.example.com//api" not in url
|
|
70
|
+
|
|
71
|
+
def test_url_with_non_standard_port(self):
|
|
72
|
+
"""Test absolute URL generation with non-standard port."""
|
|
73
|
+
mock_settings = create_mock_settings(backend_public_url="https://atlas.example.com:8443")
|
|
74
|
+
|
|
75
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
76
|
+
mock_cm.app_settings = mock_settings
|
|
77
|
+
|
|
78
|
+
url = create_download_url("test-key-abc", "user@example.com")
|
|
79
|
+
|
|
80
|
+
# Should include port in URL
|
|
81
|
+
assert url.startswith("https://atlas.example.com:8443/api/files/download/")
|
|
82
|
+
|
|
83
|
+
def test_url_with_localhost(self):
|
|
84
|
+
"""Test absolute URL generation with localhost (development mode)."""
|
|
85
|
+
mock_settings = create_mock_settings(backend_public_url="http://localhost:8000")
|
|
86
|
+
|
|
87
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
88
|
+
mock_cm.app_settings = mock_settings
|
|
89
|
+
|
|
90
|
+
url = create_download_url("test-key-dev", "dev@example.com")
|
|
91
|
+
|
|
92
|
+
# Should use localhost URL
|
|
93
|
+
assert url.startswith("http://localhost:8000/api/files/download/")
|
|
94
|
+
|
|
95
|
+
def test_fallback_without_user_email(self):
|
|
96
|
+
"""Test URL generation without user email (no token)."""
|
|
97
|
+
mock_settings = create_mock_settings(backend_public_url="https://atlas.example.com")
|
|
98
|
+
|
|
99
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
100
|
+
mock_cm.app_settings = mock_settings
|
|
101
|
+
|
|
102
|
+
url = create_download_url("test-key-nouser", None)
|
|
103
|
+
|
|
104
|
+
# Should be absolute but without token
|
|
105
|
+
assert url.startswith("https://atlas.example.com/api/files/download/")
|
|
106
|
+
assert "test-key-nouser" in url
|
|
107
|
+
assert "token=" not in url
|
|
108
|
+
|
|
109
|
+
def test_config_error_falls_back_to_relative(self):
|
|
110
|
+
"""Test that configuration errors fall back to relative URLs gracefully."""
|
|
111
|
+
# Mock config manager app_settings to raise exception when accessed
|
|
112
|
+
mock_cm = MagicMock()
|
|
113
|
+
# When app_settings is accessed, raise exception
|
|
114
|
+
type(mock_cm).app_settings = property(lambda self: (_ for _ in ()).throw(Exception("Config error")))
|
|
115
|
+
|
|
116
|
+
# Also need to mock _get_secret since it also accesses config_manager
|
|
117
|
+
with patch('atlas.core.capabilities.config_manager', mock_cm):
|
|
118
|
+
with patch('atlas.core.capabilities._get_secret', return_value=b'test-secret'):
|
|
119
|
+
url = create_download_url("test-key-error", "user@example.com")
|
|
120
|
+
|
|
121
|
+
# Should fall back to relative URL
|
|
122
|
+
assert url.startswith("/api/files/download/")
|
|
123
|
+
assert not url.startswith("http")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestTokenValidation:
|
|
127
|
+
"""Test suite for token generation and validation with absolute URLs."""
|
|
128
|
+
|
|
129
|
+
def test_token_works_with_absolute_urls(self):
|
|
130
|
+
"""Test that tokens generated for absolute URLs are valid."""
|
|
131
|
+
from atlas.core.capabilities import verify_file_token
|
|
132
|
+
|
|
133
|
+
user_email = "test@example.com"
|
|
134
|
+
file_key = "test-key-123"
|
|
135
|
+
|
|
136
|
+
mock_settings = create_mock_settings()
|
|
137
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
138
|
+
mock_cm.app_settings = mock_settings
|
|
139
|
+
|
|
140
|
+
# Generate token
|
|
141
|
+
token = generate_file_token(user_email, file_key, ttl_seconds=60)
|
|
142
|
+
|
|
143
|
+
# Verify token
|
|
144
|
+
claims = verify_file_token(token)
|
|
145
|
+
|
|
146
|
+
assert claims is not None
|
|
147
|
+
assert claims["u"] == user_email
|
|
148
|
+
assert claims["k"] == file_key
|
|
149
|
+
assert claims["e"] > 0 # Expiry timestamp exists
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestBackwardCompatibility:
|
|
153
|
+
"""Test suite to ensure backward compatibility with existing behavior."""
|
|
154
|
+
|
|
155
|
+
def test_stdio_servers_still_work_with_relative_urls(self):
|
|
156
|
+
"""Test that stdio (local) servers continue to work with relative URLs."""
|
|
157
|
+
mock_settings = create_mock_settings(backend_public_url=None)
|
|
158
|
+
|
|
159
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
160
|
+
mock_cm.app_settings = mock_settings
|
|
161
|
+
|
|
162
|
+
url = create_download_url("local-file", "local@example.com")
|
|
163
|
+
|
|
164
|
+
# Local servers can resolve relative URLs
|
|
165
|
+
assert url.startswith("/api/files/download/")
|
|
166
|
+
|
|
167
|
+
def test_existing_mcp_servers_unaffected(self):
|
|
168
|
+
"""Test that existing MCP server configurations continue to work."""
|
|
169
|
+
# This test verifies that the changes don't break existing deployments
|
|
170
|
+
# that haven't configured BACKEND_PUBLIC_URL
|
|
171
|
+
|
|
172
|
+
mock_settings = create_mock_settings(backend_public_url=None)
|
|
173
|
+
|
|
174
|
+
with patch('atlas.core.capabilities.config_manager') as mock_cm:
|
|
175
|
+
mock_cm.app_settings = mock_settings
|
|
176
|
+
|
|
177
|
+
# Should handle missing attribute gracefully and return relative URL
|
|
178
|
+
url = create_download_url("legacy-key", "legacy@example.com")
|
|
179
|
+
|
|
180
|
+
assert url.startswith("/api/files/download/")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
pytest.main([__file__, "-v"])
|
|
185
|
+
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Tests for banner message save logging functionality."""
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from main import app
|
|
5
|
+
from starlette.testclient import TestClient
|
|
6
|
+
|
|
7
|
+
from atlas.modules.config import config_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_banner_save_success_logging(caplog, tmp_path, monkeypatch):
|
|
11
|
+
"""Test that successful banner save produces INFO log with file path."""
|
|
12
|
+
client = TestClient(app)
|
|
13
|
+
|
|
14
|
+
# Setup temp config directory
|
|
15
|
+
config_dir = tmp_path / "config" / "overrides"
|
|
16
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
# Mock config path to use temp directory
|
|
19
|
+
def mock_get_admin_config_path(filename):
|
|
20
|
+
return config_dir / filename
|
|
21
|
+
|
|
22
|
+
monkeypatch.setattr(
|
|
23
|
+
"atlas.routes.admin_routes.get_admin_config_path",
|
|
24
|
+
mock_get_admin_config_path
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Mock setup_config_overrides to avoid side effects
|
|
28
|
+
monkeypatch.setattr("atlas.routes.admin_routes.setup_config_overrides", lambda: None)
|
|
29
|
+
|
|
30
|
+
# Capture logs at INFO level
|
|
31
|
+
with caplog.at_level(logging.INFO):
|
|
32
|
+
# Make request to update banner messages
|
|
33
|
+
response = client.post(
|
|
34
|
+
"/admin/banners",
|
|
35
|
+
json={"messages": ["Test banner message", "Another test message"]},
|
|
36
|
+
headers={"X-User-Email": "admin@example.com"}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Verify request succeeded
|
|
40
|
+
assert response.status_code == 200
|
|
41
|
+
|
|
42
|
+
# Check that INFO log was created with success message
|
|
43
|
+
info_logs = [record for record in caplog.records if record.levelname == "INFO"]
|
|
44
|
+
assert len(info_logs) > 0
|
|
45
|
+
|
|
46
|
+
# Find the banner save log
|
|
47
|
+
banner_logs = [
|
|
48
|
+
log for log in info_logs
|
|
49
|
+
if "Banner messages successfully saved to disk" in log.message
|
|
50
|
+
]
|
|
51
|
+
assert len(banner_logs) == 1
|
|
52
|
+
|
|
53
|
+
# Verify log contains file path and admin user
|
|
54
|
+
log_message = banner_logs[0].message
|
|
55
|
+
assert "messages.txt" in log_message
|
|
56
|
+
assert "admin@example.com" in log_message
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_banner_save_failure_logging(caplog, tmp_path, monkeypatch):
|
|
60
|
+
"""Test that failed banner save produces ERROR log with details."""
|
|
61
|
+
client = TestClient(app)
|
|
62
|
+
|
|
63
|
+
# Mock get_admin_config_path to return a path
|
|
64
|
+
readonly_file = tmp_path / "readonly.txt"
|
|
65
|
+
readonly_file.write_text("test")
|
|
66
|
+
|
|
67
|
+
def mock_get_admin_config_path(filename):
|
|
68
|
+
return readonly_file
|
|
69
|
+
|
|
70
|
+
monkeypatch.setattr(
|
|
71
|
+
"atlas.routes.admin_routes.get_admin_config_path",
|
|
72
|
+
mock_get_admin_config_path
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Mock setup_config_overrides to avoid side effects
|
|
76
|
+
monkeypatch.setattr("atlas.routes.admin_routes.setup_config_overrides", lambda: None)
|
|
77
|
+
|
|
78
|
+
# Mock write_file_content to raise an exception
|
|
79
|
+
def mock_write_file_content(file_path, content, file_type="text"):
|
|
80
|
+
raise PermissionError("Permission denied")
|
|
81
|
+
|
|
82
|
+
monkeypatch.setattr(
|
|
83
|
+
"atlas.routes.admin_routes.write_file_content",
|
|
84
|
+
mock_write_file_content
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Capture logs at ERROR level
|
|
88
|
+
with caplog.at_level(logging.ERROR):
|
|
89
|
+
# Make request to update banner messages (should fail)
|
|
90
|
+
response = client.post(
|
|
91
|
+
"/admin/banners",
|
|
92
|
+
json={"messages": ["Test banner message"]},
|
|
93
|
+
headers={"X-User-Email": "admin@example.com"}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Verify request failed
|
|
97
|
+
assert response.status_code == 500
|
|
98
|
+
|
|
99
|
+
# Check that ERROR log was created with failure message
|
|
100
|
+
error_logs = [record for record in caplog.records if record.levelname == "ERROR"]
|
|
101
|
+
assert len(error_logs) > 0
|
|
102
|
+
|
|
103
|
+
# Find the banner save error log
|
|
104
|
+
banner_error_logs = [
|
|
105
|
+
log for log in error_logs
|
|
106
|
+
if "Failed to save banner messages to disk" in log.message
|
|
107
|
+
]
|
|
108
|
+
assert len(banner_error_logs) == 1
|
|
109
|
+
|
|
110
|
+
# Verify log contains file path and error details
|
|
111
|
+
log_message = banner_error_logs[0].message
|
|
112
|
+
assert "readonly.txt" in log_message
|
|
113
|
+
assert "Permission denied" in log_message or "PermissionError" in log_message
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_banner_save_logs_sanitized_paths(caplog, tmp_path, monkeypatch):
|
|
117
|
+
"""Test that file paths in logs are sanitized to prevent log injection."""
|
|
118
|
+
client = TestClient(app)
|
|
119
|
+
|
|
120
|
+
# Setup temp config directory with a potentially malicious name
|
|
121
|
+
config_dir = tmp_path / "config" / "overrides"
|
|
122
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
# Mock config path
|
|
125
|
+
def mock_get_admin_config_path(filename):
|
|
126
|
+
return config_dir / filename
|
|
127
|
+
|
|
128
|
+
monkeypatch.setattr(
|
|
129
|
+
"atlas.routes.admin_routes.get_admin_config_path",
|
|
130
|
+
mock_get_admin_config_path
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Mock setup_config_overrides to avoid side effects
|
|
134
|
+
monkeypatch.setattr("atlas.routes.admin_routes.setup_config_overrides", lambda: None)
|
|
135
|
+
|
|
136
|
+
# Capture logs at INFO level
|
|
137
|
+
with caplog.at_level(logging.INFO):
|
|
138
|
+
# Make request to update banner messages
|
|
139
|
+
response = client.post(
|
|
140
|
+
"/admin/banners",
|
|
141
|
+
json={"messages": ["Test message"]},
|
|
142
|
+
headers={"X-User-Email": "admin@example.com"}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Verify request succeeded
|
|
146
|
+
assert response.status_code == 200
|
|
147
|
+
|
|
148
|
+
# Check that log messages don't contain raw newlines or control characters
|
|
149
|
+
info_logs = [record for record in caplog.records if record.levelname == "INFO"]
|
|
150
|
+
banner_logs = [
|
|
151
|
+
log for log in info_logs
|
|
152
|
+
if "Banner messages successfully saved to disk" in log.message
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
assert len(banner_logs) == 1
|
|
156
|
+
log_message = banner_logs[0].message
|
|
157
|
+
|
|
158
|
+
# Verify no newlines in the log message
|
|
159
|
+
assert "\n" not in log_message
|
|
160
|
+
assert "\r" not in log_message
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_banner_get_includes_enabled_status(tmp_path, monkeypatch):
|
|
164
|
+
"""Test that GET /admin/banners includes banner_enabled status."""
|
|
165
|
+
client = TestClient(app)
|
|
166
|
+
|
|
167
|
+
# Setup temp config directory
|
|
168
|
+
config_dir = tmp_path / "config" / "overrides"
|
|
169
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
messages_file = config_dir / "messages.txt"
|
|
171
|
+
messages_file.write_text("Test message\n")
|
|
172
|
+
|
|
173
|
+
# Mock config path to use temp directory
|
|
174
|
+
def mock_get_admin_config_path(filename):
|
|
175
|
+
return config_dir / filename
|
|
176
|
+
|
|
177
|
+
monkeypatch.setattr(
|
|
178
|
+
"atlas.routes.admin_routes.get_admin_config_path",
|
|
179
|
+
mock_get_admin_config_path
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Mock setup_config_overrides to avoid side effects
|
|
183
|
+
monkeypatch.setattr("atlas.routes.admin_routes.setup_config_overrides", lambda: None)
|
|
184
|
+
|
|
185
|
+
# Make request to get banner config
|
|
186
|
+
response = client.get(
|
|
187
|
+
"/admin/banners",
|
|
188
|
+
headers={"X-User-Email": "admin@example.com"}
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Verify request succeeded
|
|
192
|
+
assert response.status_code == 200
|
|
193
|
+
|
|
194
|
+
# Check response contains banner_enabled field
|
|
195
|
+
data = response.json()
|
|
196
|
+
assert "banner_enabled" in data
|
|
197
|
+
assert isinstance(data["banner_enabled"], bool)
|
|
198
|
+
# The field should match the current config setting
|
|
199
|
+
assert data["banner_enabled"] == config_manager.app_settings.banner_enabled
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_banner_get_with_feature_disabled(tmp_path, monkeypatch):
|
|
203
|
+
"""Test that GET /admin/banners returns banner_enabled: false when feature is disabled."""
|
|
204
|
+
client = TestClient(app)
|
|
205
|
+
|
|
206
|
+
# Setup temp config directory
|
|
207
|
+
config_dir = tmp_path / "config" / "overrides"
|
|
208
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
messages_file = config_dir / "messages.txt"
|
|
210
|
+
messages_file.write_text("Test message\n")
|
|
211
|
+
|
|
212
|
+
# Mock config path
|
|
213
|
+
def mock_get_admin_config_path(filename):
|
|
214
|
+
return config_dir / filename
|
|
215
|
+
|
|
216
|
+
monkeypatch.setattr(
|
|
217
|
+
"atlas.routes.admin_routes.get_admin_config_path",
|
|
218
|
+
mock_get_admin_config_path
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Mock setup_config_overrides
|
|
222
|
+
monkeypatch.setattr("atlas.routes.admin_routes.setup_config_overrides", lambda: None)
|
|
223
|
+
|
|
224
|
+
# Mock banner_enabled to be false
|
|
225
|
+
monkeypatch.setattr(
|
|
226
|
+
"atlas.routes.admin_routes.config_manager.app_settings.banner_enabled",
|
|
227
|
+
False
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Make request to get banner config
|
|
231
|
+
response = client.get(
|
|
232
|
+
"/admin/banners",
|
|
233
|
+
headers={"X-User-Email": "admin@example.com"}
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Verify request succeeded
|
|
237
|
+
assert response.status_code == 200
|
|
238
|
+
|
|
239
|
+
# Check response contains banner_enabled field set to false
|
|
240
|
+
data = response.json()
|
|
241
|
+
assert "banner_enabled" in data
|
|
242
|
+
assert data["banner_enabled"] is False
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_banner_get_with_feature_enabled(tmp_path, monkeypatch):
|
|
246
|
+
"""Test that GET /admin/banners returns banner_enabled: true when feature is enabled."""
|
|
247
|
+
client = TestClient(app)
|
|
248
|
+
|
|
249
|
+
# Setup temp config directory
|
|
250
|
+
config_dir = tmp_path / "config" / "overrides"
|
|
251
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
messages_file = config_dir / "messages.txt"
|
|
253
|
+
messages_file.write_text("Test message\n")
|
|
254
|
+
|
|
255
|
+
# Mock config path
|
|
256
|
+
def mock_get_admin_config_path(filename):
|
|
257
|
+
return config_dir / filename
|
|
258
|
+
|
|
259
|
+
monkeypatch.setattr(
|
|
260
|
+
"atlas.routes.admin_routes.get_admin_config_path",
|
|
261
|
+
mock_get_admin_config_path
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Mock setup_config_overrides
|
|
265
|
+
monkeypatch.setattr("atlas.routes.admin_routes.setup_config_overrides", lambda: None)
|
|
266
|
+
|
|
267
|
+
# Mock banner_enabled to be true
|
|
268
|
+
monkeypatch.setattr(
|
|
269
|
+
"atlas.routes.admin_routes.config_manager.app_settings.banner_enabled",
|
|
270
|
+
True
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Make request to get banner config
|
|
274
|
+
response = client.get(
|
|
275
|
+
"/admin/banners",
|
|
276
|
+
headers={"X-User-Email": "admin@example.com"}
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Verify request succeeded
|
|
280
|
+
assert response.status_code == 200
|
|
281
|
+
|
|
282
|
+
# Check response contains banner_enabled field set to true
|
|
283
|
+
data = response.json()
|
|
284
|
+
assert "banner_enabled" in data
|
|
285
|
+
assert data["banner_enabled"] is True
|
|
286
|
+
|
|
287
|
+
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import types
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from fastapi.testclient import TestClient
|
|
9
|
+
|
|
10
|
+
# Ensure backend root is on path
|
|
11
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
12
|
+
|
|
13
|
+
# Save original module before patching (so we can restore it later)
|
|
14
|
+
_ORIGINAL_LITELLM_MODULE = sys.modules.get("atlas.modules.llm.litellm_caller")
|
|
15
|
+
|
|
16
|
+
# Stub LiteLLM before importing app to avoid external dependency in tests
|
|
17
|
+
fake_litellm_caller = types.ModuleType("atlas.modules.llm.litellm_caller")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _FakeLLM:
|
|
21
|
+
def __init__(self, *args, **kwargs):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
async def call_plain(self, *args, **kwargs):
|
|
25
|
+
return "ok"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
fake_litellm_caller.LiteLLMCaller = _FakeLLM # type: ignore
|
|
29
|
+
sys.modules["atlas.modules.llm.litellm_caller"] = fake_litellm_caller
|
|
30
|
+
|
|
31
|
+
from main import app # noqa: E402 # type: ignore
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _restore_original_module():
|
|
35
|
+
"""Restore the original litellm_caller module if it was saved."""
|
|
36
|
+
if _ORIGINAL_LITELLM_MODULE is not None:
|
|
37
|
+
sys.modules["atlas.modules.llm.litellm_caller"] = _ORIGINAL_LITELLM_MODULE
|
|
38
|
+
elif "atlas.modules.llm.litellm_caller" in sys.modules:
|
|
39
|
+
del sys.modules["atlas.modules.llm.litellm_caller"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture(scope="module", autouse=True)
|
|
43
|
+
def restore_litellm_module_after_tests():
|
|
44
|
+
"""Restore the original LiteLLM module after all tests in this module complete."""
|
|
45
|
+
yield
|
|
46
|
+
_restore_original_module()
|
|
47
|
+
|
|
48
|
+
from atlas.core.capabilities import generate_file_token, verify_file_token # noqa: E402 # type: ignore
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FakeS3:
|
|
52
|
+
def __init__(self):
|
|
53
|
+
self._store = {}
|
|
54
|
+
self.endpoint_url = "mock://s3"
|
|
55
|
+
self.bucket_name = "test-bucket"
|
|
56
|
+
|
|
57
|
+
async def upload_file(self, user_email: str, filename: str, content_base64: str, content_type: str = "application/octet-stream", tags=None, source_type: str = "user"):
|
|
58
|
+
key = f"k_{len(self._store)+1}"
|
|
59
|
+
meta = {
|
|
60
|
+
"key": key,
|
|
61
|
+
"filename": filename,
|
|
62
|
+
"size": len(base64.b64decode(content_base64)),
|
|
63
|
+
"content_type": content_type,
|
|
64
|
+
"last_modified": "now",
|
|
65
|
+
"etag": "test",
|
|
66
|
+
"tags": tags or {"source": source_type},
|
|
67
|
+
"user_email": user_email,
|
|
68
|
+
"content_base64": content_base64,
|
|
69
|
+
}
|
|
70
|
+
self._store[key] = meta
|
|
71
|
+
return meta
|
|
72
|
+
|
|
73
|
+
async def get_file(self, user_email: str, file_key: str):
|
|
74
|
+
return self._store.get(file_key)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.fixture()
|
|
78
|
+
def client(monkeypatch):
|
|
79
|
+
# Inject fake S3 into app_factory for route handlers
|
|
80
|
+
from atlas.infrastructure import app_factory as af # type: ignore
|
|
81
|
+
|
|
82
|
+
fake = FakeS3()
|
|
83
|
+
|
|
84
|
+
original_get = af.get_file_storage
|
|
85
|
+
af.get_file_storage = lambda: fake # type: ignore
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
yield TestClient(app)
|
|
89
|
+
finally:
|
|
90
|
+
# Restore
|
|
91
|
+
af.get_file_storage = original_get # type: ignore
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_capability_token_roundtrip():
|
|
95
|
+
token = generate_file_token("alice@example.com", "k123", ttl_seconds=60)
|
|
96
|
+
claims = verify_file_token(token)
|
|
97
|
+
assert claims is not None
|
|
98
|
+
assert claims["u"] == "alice@example.com"
|
|
99
|
+
assert claims["k"] == "k123"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _extract_key(resp_json) -> Optional[str]:
|
|
103
|
+
if isinstance(resp_json, dict):
|
|
104
|
+
return resp_json.get("key")
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_download_with_and_without_token(client):
|
|
109
|
+
"""Test file upload/download with proper authentication in production mode."""
|
|
110
|
+
# Upload a small text file via API with authentication header
|
|
111
|
+
content = base64.b64encode(b"hello world").decode("utf-8")
|
|
112
|
+
upload_resp = client.post(
|
|
113
|
+
"/api/files",
|
|
114
|
+
json={
|
|
115
|
+
"filename": "hello.txt",
|
|
116
|
+
"content_base64": content,
|
|
117
|
+
"content_type": "text/plain",
|
|
118
|
+
"tags": {"source": "test"},
|
|
119
|
+
},
|
|
120
|
+
headers={"X-User-Email": "test@example.com"},
|
|
121
|
+
)
|
|
122
|
+
assert upload_resp.status_code == 200
|
|
123
|
+
key = _extract_key(upload_resp.json())
|
|
124
|
+
assert key
|
|
125
|
+
|
|
126
|
+
# Download with same user authentication
|
|
127
|
+
dl_resp = client.get(
|
|
128
|
+
f"/api/files/download/{key}",
|
|
129
|
+
headers={"X-User-Email": "test@example.com"}
|
|
130
|
+
)
|
|
131
|
+
assert dl_resp.status_code == 200
|
|
132
|
+
assert dl_resp.content == b"hello world"
|
|
133
|
+
|
|
134
|
+
# With capability token for a different user (token grants access)
|
|
135
|
+
token = generate_file_token("alice@example.com", key, ttl_seconds=60)
|
|
136
|
+
dl_resp2 = client.get(
|
|
137
|
+
f"/api/files/download/{key}",
|
|
138
|
+
params={"token": token},
|
|
139
|
+
headers={"X-User-Email": "alice@example.com"}
|
|
140
|
+
)
|
|
141
|
+
assert dl_resp2.status_code == 200
|
|
142
|
+
assert dl_resp2.content == b"hello world"
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_download_without_auth_fails_in_production_mode(client):
|
|
146
|
+
"""Test that requests without authentication fail in production mode (debug_mode=False).
|
|
147
|
+
|
|
148
|
+
This test validates that when debug_mode=False (production mode), requests without
|
|
149
|
+
the X-User-Email header are rejected with 401 Unauthorized.
|
|
150
|
+
|
|
151
|
+
Note: This test will pass in production mode and fail in debug mode, which is expected.
|
|
152
|
+
CI runs tests in both modes to validate both behaviors.
|
|
153
|
+
"""
|
|
154
|
+
from atlas.infrastructure.app_factory import app_factory # type: ignore
|
|
155
|
+
|
|
156
|
+
# Skip this test if running in debug mode (it's expected to fail)
|
|
157
|
+
if app_factory.config_manager.app_settings.debug_mode:
|
|
158
|
+
pytest.skip("Skipping production mode test - running in debug mode")
|
|
159
|
+
|
|
160
|
+
content = base64.b64encode(b"test content").decode("utf-8")
|
|
161
|
+
|
|
162
|
+
# Try to upload without authentication - should fail with 401 in production mode
|
|
163
|
+
upload_resp = client.post(
|
|
164
|
+
"/api/files",
|
|
165
|
+
json={
|
|
166
|
+
"filename": "test.txt",
|
|
167
|
+
"content_base64": content,
|
|
168
|
+
"content_type": "text/plain",
|
|
169
|
+
"tags": {"source": "test"},
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
assert upload_resp.status_code == 401
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_injection_produces_tokenized_urls(client, monkeypatch):
|
|
176
|
+
"""Verify that tool argument injection replaces filename with tokenized URL."""
|
|
177
|
+
from atlas.application.chat.utilities.tool_executor import inject_context_into_args # type: ignore
|
|
178
|
+
|
|
179
|
+
# Create a fake session context with a file mapping
|
|
180
|
+
session_context = {
|
|
181
|
+
"user_email": "bob@example.com",
|
|
182
|
+
"files": {
|
|
183
|
+
"report.pdf": {"key": "abc123", "content_type": "application/pdf"}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
args = {"filename": "report.pdf"}
|
|
188
|
+
injected = inject_context_into_args(args, session_context)
|
|
189
|
+
|
|
190
|
+
assert injected["username"] == "bob@example.com"
|
|
191
|
+
assert injected["original_filename"] == "report.pdf"
|
|
192
|
+
# URL should include /api/files/download/abc123 and a token query param
|
|
193
|
+
assert injected["filename"].startswith("/api/files/download/abc123")
|
|
194
|
+
assert "?token=" in injected["filename"]
|
|
195
|
+
|
|
196
|
+
# Multiple files
|
|
197
|
+
args2 = {"file_names": ["report.pdf", "missing.txt"]}
|
|
198
|
+
injected2 = inject_context_into_args(args2, session_context)
|
|
199
|
+
assert injected2["original_file_names"] == ["report.pdf", "missing.txt"]
|
|
200
|
+
assert injected2["file_names"][0].startswith("/api/files/download/abc123")
|
|
201
|
+
assert "?token=" in injected2["file_names"][0]
|
|
202
|
+
# Missing.txt doesn't resolve to key, kept as-is
|
|
203
|
+
assert injected2["file_names"][1] == "missing.txt"
|