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,224 @@
|
|
|
1
|
+
"""Integration tests for AtlasRAGClient with the mock service.
|
|
2
|
+
|
|
3
|
+
These tests require the atlas-rag-api-mock service to be running.
|
|
4
|
+
They can be skipped if the mock service is not available.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
# Add paths for imports
|
|
16
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
17
|
+
|
|
18
|
+
from atlas.modules.rag.atlas_rag_client import AtlasRAGClient
|
|
19
|
+
|
|
20
|
+
MOCK_URL = "http://localhost:8002"
|
|
21
|
+
MOCK_TOKEN = "test-atlas-rag-token"
|
|
22
|
+
MOCK_STARTUP_TIMEOUT = 10
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def is_mock_running() -> bool:
|
|
26
|
+
"""Check if the mock service is running."""
|
|
27
|
+
try:
|
|
28
|
+
response = httpx.get(f"{MOCK_URL}/health", timeout=2.0)
|
|
29
|
+
return response.status_code == 200
|
|
30
|
+
except Exception:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(scope="module")
|
|
35
|
+
def mock_service():
|
|
36
|
+
"""Start the mock service if not already running."""
|
|
37
|
+
if is_mock_running():
|
|
38
|
+
yield MOCK_URL
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# Try to start the mock service
|
|
42
|
+
mock_path = os.path.join(
|
|
43
|
+
os.path.dirname(__file__), "..", "..", "mocks", "atlas-rag-api-mock", "main.py"
|
|
44
|
+
)
|
|
45
|
+
mock_path = os.path.abspath(mock_path)
|
|
46
|
+
|
|
47
|
+
if not os.path.exists(mock_path):
|
|
48
|
+
pytest.skip(f"Mock service not found at {mock_path}")
|
|
49
|
+
|
|
50
|
+
process = subprocess.Popen(
|
|
51
|
+
[sys.executable, mock_path],
|
|
52
|
+
stdout=subprocess.PIPE,
|
|
53
|
+
stderr=subprocess.PIPE,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Wait for service to start
|
|
57
|
+
start_time = time.time()
|
|
58
|
+
while time.time() - start_time < MOCK_STARTUP_TIMEOUT:
|
|
59
|
+
if is_mock_running():
|
|
60
|
+
break
|
|
61
|
+
time.sleep(0.5)
|
|
62
|
+
else:
|
|
63
|
+
process.terminate()
|
|
64
|
+
pytest.skip("Could not start mock service")
|
|
65
|
+
|
|
66
|
+
yield MOCK_URL
|
|
67
|
+
|
|
68
|
+
# Cleanup
|
|
69
|
+
process.terminate()
|
|
70
|
+
process.wait(timeout=5)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.fixture
|
|
74
|
+
def client():
|
|
75
|
+
"""Create an AtlasRAGClient configured for the mock service."""
|
|
76
|
+
return AtlasRAGClient(
|
|
77
|
+
base_url=MOCK_URL,
|
|
78
|
+
bearer_token=MOCK_TOKEN,
|
|
79
|
+
default_model="test-model",
|
|
80
|
+
top_k=4,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestAtlasRAGIntegration:
|
|
85
|
+
"""Integration tests for AtlasRAGClient with the mock service."""
|
|
86
|
+
|
|
87
|
+
@pytest.mark.asyncio
|
|
88
|
+
async def test_discover_data_sources_success(self, mock_service, client):
|
|
89
|
+
"""Test discovering data sources for a known user."""
|
|
90
|
+
sources = await client.discover_data_sources("test@test.com")
|
|
91
|
+
|
|
92
|
+
assert len(sources) > 0
|
|
93
|
+
# test@test.com has employee, engineering, devops, admin groups - sees all sources
|
|
94
|
+
source_names = [s.name for s in sources]
|
|
95
|
+
assert "company-policies" in source_names
|
|
96
|
+
assert "technical-docs" in source_names
|
|
97
|
+
assert "product-knowledge" in source_names # Public
|
|
98
|
+
|
|
99
|
+
# Check compliance levels are returned
|
|
100
|
+
for source in sources:
|
|
101
|
+
assert source.compliance_level in ["Internal", "Public"]
|
|
102
|
+
|
|
103
|
+
@pytest.mark.asyncio
|
|
104
|
+
async def test_discover_data_sources_unknown_user(self, mock_service, client):
|
|
105
|
+
"""Test discovering data sources for an unknown user returns only public sources."""
|
|
106
|
+
sources = await client.discover_data_sources("unknown@example.com")
|
|
107
|
+
# Unknown users get public sources only
|
|
108
|
+
source_names = [s.name for s in sources]
|
|
109
|
+
assert "product-knowledge" in source_names
|
|
110
|
+
assert "company-policies" not in source_names
|
|
111
|
+
assert "technical-docs" not in source_names
|
|
112
|
+
|
|
113
|
+
@pytest.mark.asyncio
|
|
114
|
+
async def test_discover_data_sources_limited_access(self, mock_service, client):
|
|
115
|
+
"""Test that users only see corpora they have access to."""
|
|
116
|
+
# bob@example.com has employee and sales groups
|
|
117
|
+
sources = await client.discover_data_sources("bob@example.com")
|
|
118
|
+
|
|
119
|
+
source_names = [s.name for s in sources]
|
|
120
|
+
assert "company-policies" in source_names # requires employee
|
|
121
|
+
assert "product-knowledge" in source_names # Public
|
|
122
|
+
# Should NOT have access to technical-docs (requires engineering or devops)
|
|
123
|
+
assert "technical-docs" not in source_names
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_query_rag_success(self, mock_service, client):
|
|
127
|
+
"""Test successful RAG query."""
|
|
128
|
+
messages = [{"role": "user", "content": "What is the API authentication?"}]
|
|
129
|
+
|
|
130
|
+
response = await client.query_rag(
|
|
131
|
+
user_name="test@test.com",
|
|
132
|
+
data_source="technical-docs",
|
|
133
|
+
messages=messages,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
assert response.content is not None
|
|
137
|
+
assert len(response.content) > 0
|
|
138
|
+
# Should contain information about API or authentication
|
|
139
|
+
assert "API" in response.content or "authentication" in response.content.lower()
|
|
140
|
+
|
|
141
|
+
# Check metadata
|
|
142
|
+
assert response.metadata is not None
|
|
143
|
+
assert response.metadata.query_processing_time_ms >= 0
|
|
144
|
+
assert response.metadata.data_source_name == "technical-docs"
|
|
145
|
+
assert response.metadata.retrieval_method == "keyword-search"
|
|
146
|
+
assert len(response.metadata.documents_found) > 0
|
|
147
|
+
|
|
148
|
+
@pytest.mark.asyncio
|
|
149
|
+
async def test_query_rag_with_metadata(self, mock_service, client):
|
|
150
|
+
"""Test that RAG query returns document metadata."""
|
|
151
|
+
messages = [{"role": "user", "content": "Tell me about deployment pipeline"}]
|
|
152
|
+
|
|
153
|
+
response = await client.query_rag(
|
|
154
|
+
user_name="charlie@example.com", # Has employee, engineering, devops groups
|
|
155
|
+
data_source="technical-docs",
|
|
156
|
+
messages=messages,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
assert response.metadata is not None
|
|
160
|
+
assert len(response.metadata.documents_found) > 0
|
|
161
|
+
|
|
162
|
+
# Check document metadata structure
|
|
163
|
+
doc = response.metadata.documents_found[0]
|
|
164
|
+
assert doc.source is not None
|
|
165
|
+
assert doc.confidence_score > 0
|
|
166
|
+
assert doc.content_type is not None
|
|
167
|
+
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
async def test_query_rag_access_denied(self, mock_service, client):
|
|
170
|
+
"""Test that RAG query returns 403 for unauthorized access."""
|
|
171
|
+
messages = [{"role": "user", "content": "Show me technical docs"}]
|
|
172
|
+
|
|
173
|
+
with pytest.raises(Exception) as exc_info:
|
|
174
|
+
await client.query_rag(
|
|
175
|
+
user_name="bob@example.com", # Has employee, sales - no engineering/devops
|
|
176
|
+
data_source="technical-docs", # Requires engineering or devops
|
|
177
|
+
messages=messages,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Should raise HTTPException with 403
|
|
181
|
+
assert "403" in str(exc_info.value) or "access" in str(exc_info.value).lower()
|
|
182
|
+
|
|
183
|
+
@pytest.mark.asyncio
|
|
184
|
+
async def test_query_rag_corpus_not_found(self, mock_service, client):
|
|
185
|
+
"""Test that RAG query returns 404 for non-existent corpus."""
|
|
186
|
+
messages = [{"role": "user", "content": "Search something"}]
|
|
187
|
+
|
|
188
|
+
with pytest.raises(Exception) as exc_info:
|
|
189
|
+
await client.query_rag(
|
|
190
|
+
user_name="test@test.com",
|
|
191
|
+
data_source="non-existent-corpus",
|
|
192
|
+
messages=messages,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Should raise HTTPException with 404
|
|
196
|
+
assert "404" in str(exc_info.value) or "not found" in str(exc_info.value).lower()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestAtlasRAGAuthFailures:
|
|
200
|
+
"""Test authentication failure scenarios."""
|
|
201
|
+
|
|
202
|
+
@pytest.mark.asyncio
|
|
203
|
+
async def test_missing_token(self, mock_service):
|
|
204
|
+
"""Test that requests without token fail with 401."""
|
|
205
|
+
client = AtlasRAGClient(
|
|
206
|
+
base_url=MOCK_URL,
|
|
207
|
+
bearer_token=None, # No token
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Discovery should return empty list on auth failure (graceful degradation)
|
|
211
|
+
sources = await client.discover_data_sources("test@test.com")
|
|
212
|
+
assert sources == []
|
|
213
|
+
|
|
214
|
+
@pytest.mark.asyncio
|
|
215
|
+
async def test_invalid_token(self, mock_service):
|
|
216
|
+
"""Test that requests with invalid token fail with 401."""
|
|
217
|
+
client = AtlasRAGClient(
|
|
218
|
+
base_url=MOCK_URL,
|
|
219
|
+
bearer_token="invalid-token",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Discovery should return empty list on auth failure (graceful degradation)
|
|
223
|
+
sources = await client.discover_data_sources("test@test.com")
|
|
224
|
+
assert sources == []
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from atlas.application.chat.service import ChatService
|
|
7
|
+
from atlas.modules.file_storage.manager import FileManager
|
|
8
|
+
from atlas.modules.file_storage.mock_s3_client import MockS3StorageClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FakeLLM:
|
|
12
|
+
async def call_plain(self, model_name, messages, temperature=0.7):
|
|
13
|
+
return "ok"
|
|
14
|
+
|
|
15
|
+
async def call_with_tools(self, model_name, messages, tools_schema, tool_choice="auto", temperature=0.7):
|
|
16
|
+
from atlas.interfaces.llm import LLMResponse
|
|
17
|
+
return LLMResponse(content="ok", tool_calls=None, model_used=model_name)
|
|
18
|
+
|
|
19
|
+
async def call_with_rag(self, model_name, messages, data_sources, user_email, temperature=0.7):
|
|
20
|
+
return "ok"
|
|
21
|
+
|
|
22
|
+
async def call_with_rag_and_tools(self, model_name, messages, data_sources, tools_schema, user_email, tool_choice="auto", temperature=0.7):
|
|
23
|
+
from atlas.interfaces.llm import LLMResponse
|
|
24
|
+
return LLMResponse(content="ok", tool_calls=None, model_used=model_name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def file_manager():
|
|
29
|
+
# Use in-process mock S3 for deterministic tests
|
|
30
|
+
return FileManager(s3_client=MockS3StorageClient())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def chat_service(file_manager):
|
|
35
|
+
# Minimal ChatService wiring for file/session operations
|
|
36
|
+
return ChatService(llm=FakeLLM(), file_manager=file_manager)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_handle_attach_file_success_creates_session_and_emits_update(chat_service, file_manager):
|
|
41
|
+
user_email = "user1@example.com"
|
|
42
|
+
session_id = uuid.uuid4()
|
|
43
|
+
|
|
44
|
+
# Seed a file into the mock storage for this user
|
|
45
|
+
filename = "report.txt"
|
|
46
|
+
content_b64 = base64.b64encode(b"hello world").decode()
|
|
47
|
+
upload_meta = await file_manager.s3_client.upload_file(
|
|
48
|
+
user_email=user_email,
|
|
49
|
+
filename=filename,
|
|
50
|
+
content_base64=content_b64,
|
|
51
|
+
content_type="text/plain",
|
|
52
|
+
tags={"source": "user"},
|
|
53
|
+
source_type="user",
|
|
54
|
+
)
|
|
55
|
+
s3_key = upload_meta["key"]
|
|
56
|
+
|
|
57
|
+
updates = []
|
|
58
|
+
|
|
59
|
+
async def capture_update(msg):
|
|
60
|
+
updates.append(msg)
|
|
61
|
+
|
|
62
|
+
# Act: attach the file to a brand new session (auto-creates session)
|
|
63
|
+
resp = await chat_service.handle_attach_file(
|
|
64
|
+
session_id=session_id,
|
|
65
|
+
s3_key=s3_key,
|
|
66
|
+
user_email=user_email,
|
|
67
|
+
update_callback=capture_update,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Assert: success response and files_update emitted
|
|
71
|
+
assert resp.get("type") == "file_attach"
|
|
72
|
+
assert resp.get("success") is True
|
|
73
|
+
assert resp.get("filename") == filename
|
|
74
|
+
|
|
75
|
+
assert any(
|
|
76
|
+
u.get("type") == "intermediate_update" and u.get("update_type") == "files_update"
|
|
77
|
+
for u in updates
|
|
78
|
+
), "Expected a files_update intermediate update to be emitted"
|
|
79
|
+
|
|
80
|
+
# Session context should include the file by filename
|
|
81
|
+
session = chat_service.sessions.get(session_id)
|
|
82
|
+
assert session is not None
|
|
83
|
+
assert filename in session.context.get("files", {})
|
|
84
|
+
assert session.context["files"][filename]["key"] == s3_key
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.mark.asyncio
|
|
88
|
+
async def test_handle_attach_file_not_found_returns_error(chat_service):
|
|
89
|
+
user_email = "user1@example.com"
|
|
90
|
+
session_id = uuid.uuid4()
|
|
91
|
+
|
|
92
|
+
# Non-existent S3 key for the same user
|
|
93
|
+
bad_key = f"users/{user_email}/uploads/does_not_exist_12345.txt"
|
|
94
|
+
resp = await chat_service.handle_attach_file(
|
|
95
|
+
session_id=session_id,
|
|
96
|
+
s3_key=bad_key,
|
|
97
|
+
user_email=user_email,
|
|
98
|
+
update_callback=None,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert resp.get("type") == "file_attach"
|
|
102
|
+
assert resp.get("success") is False
|
|
103
|
+
assert "File not found" in resp.get("error", "")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.asyncio
|
|
107
|
+
async def test_handle_attach_file_unauthorized_other_user_key(chat_service, file_manager):
|
|
108
|
+
# Upload under user1
|
|
109
|
+
owner_email = "owner@example.com"
|
|
110
|
+
other_email = "other@example.com"
|
|
111
|
+
session_id = uuid.uuid4()
|
|
112
|
+
|
|
113
|
+
filename = "secret.pdf"
|
|
114
|
+
content_b64 = base64.b64encode(b"top-secret").decode()
|
|
115
|
+
upload_meta = await file_manager.s3_client.upload_file(
|
|
116
|
+
user_email=owner_email,
|
|
117
|
+
filename=filename,
|
|
118
|
+
content_base64=content_b64,
|
|
119
|
+
content_type="application/pdf",
|
|
120
|
+
tags={"source": "user"},
|
|
121
|
+
source_type="user",
|
|
122
|
+
)
|
|
123
|
+
s3_key = upload_meta["key"]
|
|
124
|
+
|
|
125
|
+
# Attempt to attach with a different user should fail
|
|
126
|
+
resp = await chat_service.handle_attach_file(
|
|
127
|
+
session_id=session_id,
|
|
128
|
+
s3_key=s3_key,
|
|
129
|
+
user_email=other_email,
|
|
130
|
+
update_callback=None,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
assert resp.get("type") == "file_attach"
|
|
134
|
+
assert resp.get("success") is False
|
|
135
|
+
assert "Access denied" in resp.get("error", "")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.asyncio
|
|
139
|
+
async def test_handle_reset_session_reinitializes(chat_service):
|
|
140
|
+
user_email = "user1@example.com"
|
|
141
|
+
session_id = uuid.uuid4()
|
|
142
|
+
|
|
143
|
+
# Create a session first
|
|
144
|
+
await chat_service.create_session(session_id, user_email)
|
|
145
|
+
assert chat_service.sessions.get(session_id) is not None
|
|
146
|
+
|
|
147
|
+
# Reset the session
|
|
148
|
+
resp = await chat_service.handle_reset_session(session_id=session_id, user_email=user_email)
|
|
149
|
+
|
|
150
|
+
assert resp.get("type") == "session_reset"
|
|
151
|
+
# After reset, a fresh active session should exist for the same id
|
|
152
|
+
new_session = chat_service.sessions.get(session_id)
|
|
153
|
+
assert new_session is not None
|
|
154
|
+
assert new_session.active is True
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.asyncio
|
|
158
|
+
async def test_handle_download_file_success_after_attach(chat_service, file_manager):
|
|
159
|
+
user_email = "user1@example.com"
|
|
160
|
+
session_id = uuid.uuid4()
|
|
161
|
+
|
|
162
|
+
# Upload and then attach to session
|
|
163
|
+
filename = "notes.md"
|
|
164
|
+
content_bytes = b"### Title\nSome content."
|
|
165
|
+
content_b64 = base64.b64encode(content_bytes).decode()
|
|
166
|
+
upload_meta = await file_manager.s3_client.upload_file(
|
|
167
|
+
user_email=user_email,
|
|
168
|
+
filename=filename,
|
|
169
|
+
content_base64=content_b64,
|
|
170
|
+
content_type="text/markdown",
|
|
171
|
+
tags={"source": "user"},
|
|
172
|
+
source_type="user",
|
|
173
|
+
)
|
|
174
|
+
s3_key = upload_meta["key"]
|
|
175
|
+
|
|
176
|
+
await chat_service.handle_attach_file(
|
|
177
|
+
session_id=session_id,
|
|
178
|
+
s3_key=s3_key,
|
|
179
|
+
user_email=user_email,
|
|
180
|
+
update_callback=None,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Act: download by filename (from session context)
|
|
184
|
+
resp = await chat_service.handle_download_file(
|
|
185
|
+
session_id=session_id,
|
|
186
|
+
filename=filename,
|
|
187
|
+
user_email=user_email,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert resp.get("type") is not None
|
|
191
|
+
# content_base64 should match uploaded content
|
|
192
|
+
returned_b64 = resp.get("content_base64")
|
|
193
|
+
assert isinstance(returned_b64, str) and len(returned_b64) > 0
|
|
194
|
+
assert base64.b64decode(returned_b64) == content_bytes
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@pytest.mark.asyncio
|
|
198
|
+
async def test_handle_download_file_not_in_session_returns_error(chat_service):
|
|
199
|
+
user_email = "user1@example.com"
|
|
200
|
+
session_id = uuid.uuid4()
|
|
201
|
+
filename = "missing.txt"
|
|
202
|
+
|
|
203
|
+
# No attach performed; should error that file isn't in session
|
|
204
|
+
resp = await chat_service.handle_download_file(
|
|
205
|
+
session_id=session_id,
|
|
206
|
+
filename=filename,
|
|
207
|
+
user_email=user_email,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert resp.get("error") == "Session or file manager not available" or resp.get("error") == "File not found in session"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@pytest.mark.asyncio
|
|
214
|
+
async def test_upload_file_with_spaces_in_filename(file_manager):
|
|
215
|
+
"""Files with spaces in their names should upload successfully after sanitization."""
|
|
216
|
+
user_email = "user1@example.com"
|
|
217
|
+
filename_with_spaces = "my report file.txt"
|
|
218
|
+
content_b64 = base64.b64encode(b"some content").decode()
|
|
219
|
+
|
|
220
|
+
result = await file_manager.upload_file(
|
|
221
|
+
user_email=user_email,
|
|
222
|
+
filename=filename_with_spaces,
|
|
223
|
+
content_base64=content_b64,
|
|
224
|
+
source_type="user",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Filename should be sanitized (spaces replaced with underscores)
|
|
228
|
+
assert result["filename"] == "my_report_file.txt"
|
|
229
|
+
assert "my_report_file.txt" in result["key"]
|
|
230
|
+
assert " " not in result["key"]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@pytest.mark.asyncio
|
|
234
|
+
async def test_upload_multiple_files_with_spaces(file_manager):
|
|
235
|
+
"""upload_multiple_files should sanitize filenames containing spaces."""
|
|
236
|
+
user_email = "user1@example.com"
|
|
237
|
+
files = {
|
|
238
|
+
"my document.pdf": base64.b64encode(b"pdf bytes").decode(),
|
|
239
|
+
"another file.txt": base64.b64encode(b"text bytes").decode(),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
uploaded = await file_manager.upload_multiple_files(
|
|
243
|
+
user_email=user_email,
|
|
244
|
+
files=files,
|
|
245
|
+
source_type="user",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
assert "my_document.pdf" in uploaded
|
|
249
|
+
assert "another_file.txt" in uploaded
|
|
250
|
+
for key in uploaded.values():
|
|
251
|
+
assert " " not in key
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.mark.asyncio
|
|
255
|
+
async def test_attach_file_with_spaces_end_to_end(chat_service, file_manager):
|
|
256
|
+
"""Full flow: upload a file with spaces, attach it, verify sanitized name in session."""
|
|
257
|
+
user_email = "user1@example.com"
|
|
258
|
+
session_id = uuid.uuid4()
|
|
259
|
+
filename_with_spaces = "test report.txt"
|
|
260
|
+
content_b64 = base64.b64encode(b"hello spaces").decode()
|
|
261
|
+
|
|
262
|
+
upload_meta = await file_manager.upload_file(
|
|
263
|
+
user_email=user_email,
|
|
264
|
+
filename=filename_with_spaces,
|
|
265
|
+
content_base64=content_b64,
|
|
266
|
+
source_type="user",
|
|
267
|
+
)
|
|
268
|
+
s3_key = upload_meta["key"]
|
|
269
|
+
|
|
270
|
+
resp = await chat_service.handle_attach_file(
|
|
271
|
+
session_id=session_id,
|
|
272
|
+
s3_key=s3_key,
|
|
273
|
+
user_email=user_email,
|
|
274
|
+
update_callback=None,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
assert resp.get("success") is True
|
|
278
|
+
assert " " not in s3_key
|
|
279
|
+
|
|
280
|
+
# Verify the session stores the sanitized filename (no spaces)
|
|
281
|
+
session = chat_service.sessions.get(session_id)
|
|
282
|
+
assert session is not None
|
|
283
|
+
session_files = session.context.get("files", {})
|
|
284
|
+
# The filename key in the session should have underscores, not spaces
|
|
285
|
+
stored_names = list(session_files.keys())
|
|
286
|
+
for name in stored_names:
|
|
287
|
+
assert " " not in name, f"Session stored filename with spaces: {name}"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Tests for auth_utils module."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from atlas.core.authorization_manager import AuthorizationManager, create_authorization_manager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestAuthorizationManager:
|
|
11
|
+
"""Test suite for AuthorizationManager class."""
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def mock_auth_check_func(self):
|
|
15
|
+
"""Create a mock auth check function."""
|
|
16
|
+
return AsyncMock()
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_app_settings(self):
|
|
20
|
+
"""Create mock app settings."""
|
|
21
|
+
settings = MagicMock()
|
|
22
|
+
settings.admin_group = "admin"
|
|
23
|
+
return settings
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def auth_manager(self, mock_auth_check_func, mock_app_settings):
|
|
27
|
+
"""Create an AuthorizationManager instance with mocked dependencies."""
|
|
28
|
+
with patch('atlas.core.authorization_manager.get_app_settings', return_value=mock_app_settings):
|
|
29
|
+
return AuthorizationManager(mock_auth_check_func)
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
async def test_is_admin_returns_true_for_admin_user(self, auth_manager, mock_auth_check_func):
|
|
33
|
+
"""Test that is_admin returns True when auth check function returns True."""
|
|
34
|
+
# Arrange
|
|
35
|
+
user_email = "admin@example.com"
|
|
36
|
+
mock_auth_check_func.return_value = True
|
|
37
|
+
|
|
38
|
+
# Act
|
|
39
|
+
result = await auth_manager.is_admin(user_email)
|
|
40
|
+
|
|
41
|
+
# Assert
|
|
42
|
+
assert result is True
|
|
43
|
+
mock_auth_check_func.assert_called_once_with(user_email, "admin")
|
|
44
|
+
|
|
45
|
+
@pytest.mark.asyncio
|
|
46
|
+
async def test_is_admin_returns_false_for_non_admin_user(self, auth_manager, mock_auth_check_func):
|
|
47
|
+
"""Test that is_admin returns False when auth check function returns False."""
|
|
48
|
+
# Arrange
|
|
49
|
+
user_email = "user@example.com"
|
|
50
|
+
mock_auth_check_func.return_value = False
|
|
51
|
+
|
|
52
|
+
# Act
|
|
53
|
+
result = await auth_manager.is_admin(user_email)
|
|
54
|
+
|
|
55
|
+
# Assert
|
|
56
|
+
assert result is False
|
|
57
|
+
mock_auth_check_func.assert_called_once_with(user_email, "admin")
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_is_admin_uses_correct_admin_group(self, mock_auth_check_func):
|
|
61
|
+
"""Test that is_admin uses the admin group from app settings."""
|
|
62
|
+
# Arrange
|
|
63
|
+
custom_admin_group = "super_admin"
|
|
64
|
+
mock_settings = MagicMock()
|
|
65
|
+
mock_settings.admin_group = custom_admin_group
|
|
66
|
+
|
|
67
|
+
with patch('atlas.core.authorization_manager.get_app_settings', return_value=mock_settings):
|
|
68
|
+
auth_manager = AuthorizationManager(mock_auth_check_func)
|
|
69
|
+
|
|
70
|
+
user_email = "admin@example.com"
|
|
71
|
+
mock_auth_check_func.return_value = True
|
|
72
|
+
|
|
73
|
+
# Act
|
|
74
|
+
await auth_manager.is_admin(user_email)
|
|
75
|
+
|
|
76
|
+
# Assert
|
|
77
|
+
mock_auth_check_func.assert_called_once_with(user_email, custom_admin_group)
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_is_admin_handles_auth_check_exception(self, auth_manager, mock_auth_check_func):
|
|
81
|
+
"""Test that is_admin properly propagates exceptions from auth check function."""
|
|
82
|
+
# Arrange
|
|
83
|
+
user_email = "user@example.com"
|
|
84
|
+
mock_auth_check_func.side_effect = Exception("Auth service unavailable")
|
|
85
|
+
|
|
86
|
+
# Act & Assert
|
|
87
|
+
with pytest.raises(Exception, match="Auth service unavailable"):
|
|
88
|
+
await auth_manager.is_admin(user_email)
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_is_admin_with_empty_email(self, auth_manager, mock_auth_check_func):
|
|
92
|
+
"""Test that is_admin handles empty email addresses."""
|
|
93
|
+
# Arrange
|
|
94
|
+
user_email = ""
|
|
95
|
+
mock_auth_check_func.return_value = False
|
|
96
|
+
|
|
97
|
+
# Act
|
|
98
|
+
result = await auth_manager.is_admin(user_email)
|
|
99
|
+
|
|
100
|
+
# Assert
|
|
101
|
+
assert result is False
|
|
102
|
+
mock_auth_check_func.assert_called_once_with("", "admin")
|
|
103
|
+
|
|
104
|
+
@pytest.mark.asyncio
|
|
105
|
+
async def test_is_admin_with_none_email(self, auth_manager, mock_auth_check_func):
|
|
106
|
+
"""Test that is_admin handles None email addresses."""
|
|
107
|
+
# Arrange
|
|
108
|
+
user_email = None
|
|
109
|
+
mock_auth_check_func.return_value = False
|
|
110
|
+
|
|
111
|
+
# Act
|
|
112
|
+
result = await auth_manager.is_admin(user_email)
|
|
113
|
+
|
|
114
|
+
# Assert
|
|
115
|
+
assert result is False
|
|
116
|
+
mock_auth_check_func.assert_called_once_with(None, "admin")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestCreateAuthorizationManager:
|
|
120
|
+
"""Test suite for create_authorization_manager factory function."""
|
|
121
|
+
|
|
122
|
+
def test_create_authorization_manager_returns_instance(self):
|
|
123
|
+
"""Test that factory function returns an AuthorizationManager instance."""
|
|
124
|
+
# Arrange
|
|
125
|
+
mock_auth_check_func = AsyncMock()
|
|
126
|
+
|
|
127
|
+
# Act
|
|
128
|
+
with patch('atlas.core.authorization_manager.get_app_settings'):
|
|
129
|
+
result = create_authorization_manager(mock_auth_check_func)
|
|
130
|
+
|
|
131
|
+
# Assert
|
|
132
|
+
assert isinstance(result, AuthorizationManager)
|
|
133
|
+
assert result.auth_check_func is mock_auth_check_func
|
|
134
|
+
|
|
135
|
+
def test_create_authorization_manager_initializes_app_settings(self):
|
|
136
|
+
"""Test that factory function properly initializes app settings."""
|
|
137
|
+
# Arrange
|
|
138
|
+
mock_auth_check_func = AsyncMock()
|
|
139
|
+
mock_settings = MagicMock()
|
|
140
|
+
mock_settings.admin_group = "test_admin"
|
|
141
|
+
|
|
142
|
+
# Act
|
|
143
|
+
with patch('atlas.core.authorization_manager.get_app_settings', return_value=mock_settings) as mock_get_settings:
|
|
144
|
+
result = create_authorization_manager(mock_auth_check_func)
|
|
145
|
+
|
|
146
|
+
# Assert
|
|
147
|
+
mock_get_settings.assert_called_once()
|
|
148
|
+
assert result.app_settings is mock_settings
|
|
149
|
+
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_created_manager_works_correctly(self):
|
|
152
|
+
"""Test that the manager created by factory function works correctly."""
|
|
153
|
+
# Arrange
|
|
154
|
+
mock_auth_check_func = AsyncMock(return_value=True)
|
|
155
|
+
mock_settings = MagicMock()
|
|
156
|
+
mock_settings.admin_group = "admin"
|
|
157
|
+
|
|
158
|
+
# Act
|
|
159
|
+
with patch('atlas.core.authorization_manager.get_app_settings', return_value=mock_settings):
|
|
160
|
+
manager = create_authorization_manager(mock_auth_check_func)
|
|
161
|
+
result = await manager.is_admin("test@example.com")
|
|
162
|
+
|
|
163
|
+
# Assert
|
|
164
|
+
assert result is True
|
|
165
|
+
mock_auth_check_func.assert_called_once_with("test@example.com", "admin")
|