-
Notifications
You must be signed in to change notification settings - Fork 188
Expand file tree
/
Copy pathtest_cloud_authentication.py
More file actions
239 lines (197 loc) · 10.2 KB
/
test_cloud_authentication.py
File metadata and controls
239 lines (197 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""Tests for cloud authentication and subscription validation."""
from unittest.mock import AsyncMock, Mock, patch
import httpx
import pytest
from typer.testing import CliRunner
from basic_memory.cli.app import app
from basic_memory.cli.commands.cloud.api_client import (
CloudAPIError,
SubscriptionRequiredError,
make_api_request,
)
class TestAPIClientErrorHandling:
"""Tests for API client error handling."""
@pytest.mark.asyncio
async def test_parse_subscription_required_error(self):
"""Test parsing 403 subscription_required error response."""
# Mock httpx response with subscription error
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 403
mock_response.json.return_value = {
"detail": {
"error": "subscription_required",
"message": "Active subscription required for CLI access",
"subscribe_url": "https://basicmemory.com/subscribe",
}
}
mock_response.headers = {}
# Create HTTPStatusError with the mock response
http_error = httpx.HTTPStatusError("403 Forbidden", request=Mock(), response=mock_response)
# Mock httpx client to raise the error
with patch("basic_memory.cli.commands.cloud.api_client.httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.request = AsyncMock(side_effect=http_error)
mock_client.return_value.__aenter__.return_value = mock_instance
# Mock auth to return a token
with patch(
"basic_memory.cli.commands.cloud.api_client.get_authenticated_headers",
return_value={"Authorization": "Bearer test-token"},
):
# Should raise SubscriptionRequiredError
with pytest.raises(SubscriptionRequiredError) as exc_info:
await make_api_request("GET", "https://test.com/api/endpoint")
# Verify exception details
error = exc_info.value
assert error.status_code == 403
assert error.subscribe_url == "https://basicmemory.com/subscribe"
assert "Active subscription required" in str(error)
@pytest.mark.asyncio
async def test_parse_subscription_required_error_flat_format(self):
"""Test parsing 403 subscription_required error in flat format (backward compatibility)."""
# Mock httpx response with subscription error in flat format
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 403
mock_response.json.return_value = {
"error": "subscription_required",
"message": "Active subscription required",
"subscribe_url": "https://basicmemory.com/subscribe",
}
mock_response.headers = {}
# Create HTTPStatusError with the mock response
http_error = httpx.HTTPStatusError("403 Forbidden", request=Mock(), response=mock_response)
# Mock httpx client to raise the error
with patch("basic_memory.cli.commands.cloud.api_client.httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.request = AsyncMock(side_effect=http_error)
mock_client.return_value.__aenter__.return_value = mock_instance
# Mock auth to return a token
with patch(
"basic_memory.cli.commands.cloud.api_client.get_authenticated_headers",
return_value={"Authorization": "Bearer test-token"},
):
# Should raise SubscriptionRequiredError
with pytest.raises(SubscriptionRequiredError) as exc_info:
await make_api_request("GET", "https://test.com/api/endpoint")
# Verify exception details
error = exc_info.value
assert error.status_code == 403
assert error.subscribe_url == "https://basicmemory.com/subscribe"
@pytest.mark.asyncio
async def test_parse_generic_403_error(self):
"""Test parsing 403 error without subscription_required flag."""
# Mock httpx response with generic 403 error
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 403
mock_response.json.return_value = {
"error": "forbidden",
"message": "Access denied",
}
mock_response.headers = {}
# Create HTTPStatusError with the mock response
http_error = httpx.HTTPStatusError("403 Forbidden", request=Mock(), response=mock_response)
# Mock httpx client to raise the error
with patch("basic_memory.cli.commands.cloud.api_client.httpx.AsyncClient") as mock_client:
mock_instance = AsyncMock()
mock_instance.request = AsyncMock(side_effect=http_error)
mock_client.return_value.__aenter__.return_value = mock_instance
# Mock auth to return a token
with patch(
"basic_memory.cli.commands.cloud.api_client.get_authenticated_headers",
return_value={"Authorization": "Bearer test-token"},
):
# Should raise generic CloudAPIError
with pytest.raises(CloudAPIError) as exc_info:
await make_api_request("GET", "https://test.com/api/endpoint")
# Should not be a SubscriptionRequiredError
error = exc_info.value
assert not isinstance(error, SubscriptionRequiredError)
assert error.status_code == 403
class TestLoginCommand:
"""Tests for cloud login command with subscription validation."""
def test_login_without_subscription_shows_error(self):
"""Test login command displays error when subscription is required."""
runner = CliRunner()
# Mock successful OAuth login
mock_auth = AsyncMock()
mock_auth.login = AsyncMock(return_value=True)
# Mock API request to raise SubscriptionRequiredError
async def mock_make_api_request(*args, **kwargs):
raise SubscriptionRequiredError(
message="Active subscription required for CLI access",
subscribe_url="https://basicmemory.com/subscribe",
)
with patch("basic_memory.cli.commands.cloud.core_commands.CLIAuth", return_value=mock_auth):
with patch(
"basic_memory.cli.commands.cloud.core_commands.make_api_request",
side_effect=mock_make_api_request,
):
with patch(
"basic_memory.cli.commands.cloud.core_commands.get_cloud_config",
return_value=("client_id", "domain", "https://cloud.example.com"),
):
# Run login command
result = runner.invoke(app, ["cloud", "login"])
# Should exit with error
assert result.exit_code == 1
# Should display subscription error
assert "Subscription Required" in result.stdout
assert "Active subscription required" in result.stdout
assert "https://basicmemory.com/subscribe" in result.stdout
assert "bm cloud login" in result.stdout
def test_login_with_subscription_succeeds(self):
"""Test login command succeeds when user has active subscription."""
runner = CliRunner()
# Mock successful OAuth login
mock_auth = AsyncMock()
mock_auth.login = AsyncMock(return_value=True)
# Mock successful API request (subscription valid)
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.json.return_value = {"status": "healthy"}
async def mock_make_api_request(*args, **kwargs):
return mock_response
with patch("basic_memory.cli.commands.cloud.core_commands.CLIAuth", return_value=mock_auth):
with patch(
"basic_memory.cli.commands.cloud.core_commands.make_api_request",
side_effect=mock_make_api_request,
):
with patch(
"basic_memory.cli.commands.cloud.core_commands.get_cloud_config",
return_value=("client_id", "domain", "https://cloud.example.com"),
):
# Mock ConfigManager to avoid writing to real config
mock_config_manager = Mock()
mock_config = Mock()
mock_config.cloud_mode = False
mock_config_manager.load_config.return_value = mock_config
mock_config_manager.config = mock_config
with patch(
"basic_memory.cli.commands.cloud.core_commands.ConfigManager",
return_value=mock_config_manager,
):
# Run login command
result = runner.invoke(app, ["cloud", "login"])
# Should succeed
assert result.exit_code == 0
# Should enable cloud mode
assert mock_config.cloud_mode is True
mock_config_manager.save_config.assert_called_once()
# Should display success message
assert "Cloud mode enabled" in result.stdout
def test_login_authentication_failure(self):
"""Test login command handles authentication failure."""
runner = CliRunner()
# Mock failed OAuth login
mock_auth = AsyncMock()
mock_auth.login = AsyncMock(return_value=False)
with patch("basic_memory.cli.commands.cloud.core_commands.CLIAuth", return_value=mock_auth):
with patch(
"basic_memory.cli.commands.cloud.core_commands.get_cloud_config",
return_value=("client_id", "domain", "https://cloud.example.com"),
):
# Run login command
result = runner.invoke(app, ["cloud", "login"])
# Should exit with error
assert result.exit_code == 1
# Should display login failed message
assert "Login failed" in result.stdout