Skip to main content
Glama

basic-memory

test_cloud_authentication.py10.4 kB
"""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

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/basicmachines-co/basic-memory'

If you have feedback or need assistance with the MCP directory API, please join our Discord server