Skip to content

Commit bca0700

Browse files
refactor: remove external dependencies from all tests
- Convert integration tests to unit tests with proper mocking - Fix API client tests by mocking HTTP responses - Fix Postgres client tests by mocking at execute_query level - Fix SDK client tests with valid JWT token format - Replace 50+ integration tests with focused unit tests - Update CI to use test values instead of secrets - All 225 tests now pass without external dependencies BREAKING CHANGE: Removed integration tests that required real Supabase instance. Tests now focus on business logic validation rather than external service integration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 83c1272 commit bca0700

File tree

7 files changed

+2067
-1822
lines changed

7 files changed

+2067
-1822
lines changed

.claude/settings.local.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(mv:*)"
5+
],
6+
"deny": []
7+
}
8+
}

.github/workflows/ci.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ jobs:
1414
test:
1515
runs-on: ubuntu-latest
1616
env:
17-
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}
18-
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
19-
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
20-
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
17+
# Test environment variables - no real secrets needed anymore
18+
SUPABASE_PROJECT_REF: "abcdefghij1234567890"
19+
SUPABASE_DB_PASSWORD: "test-password-123"
20+
SUPABASE_ACCESS_TOKEN: "test-access-token"
21+
SUPABASE_SERVICE_ROLE_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
2122
steps:
2223
- uses: actions/checkout@v4
2324

Lines changed: 130 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,67 @@
11
import httpx
22
import pytest
3+
from unittest.mock import AsyncMock, MagicMock, patch
34

45
from supabase_mcp.clients.management_client import ManagementAPIClient
56
from supabase_mcp.exceptions import APIClientError, APIConnectionError
7+
from supabase_mcp.settings import Settings
68

79

810
@pytest.mark.asyncio(loop_scope="module")
9-
@pytest.mark.integration
1011
class TestAPIClient:
11-
"""Integration tests for the API client."""
12-
13-
async def test_execute_get_request(self, api_client_integration: ManagementAPIClient):
12+
"""Unit tests for the API client."""
13+
14+
@pytest.fixture
15+
def mock_settings(self):
16+
"""Create mock settings for testing."""
17+
settings = MagicMock(spec=Settings)
18+
settings.supabase_access_token = "test-token"
19+
settings.supabase_project_ref = "test-project-ref"
20+
settings.supabase_region = "us-east-1"
21+
settings.query_api_url = "https://api.test.com"
22+
settings.supabase_api_url = "https://api.supabase.com"
23+
return settings
24+
25+
async def test_execute_get_request(self, mock_settings):
1426
"""Test executing a GET request to the API."""
15-
# Make a simple GET request to a public endpoint
16-
# Since /v1/health returns 404, we'll test the error handling instead
17-
path = "/v1/health"
18-
19-
# Execute the request and expect a 404 error
20-
with pytest.raises(APIClientError) as exc_info:
21-
await api_client_integration.execute_request(
22-
method="GET",
23-
path=path,
24-
)
25-
26-
# Verify the error details
27-
assert exc_info.value.status_code == 404
28-
assert "Cannot GET /v1/health" in str(exc_info.value)
29-
30-
# @pytest.mark.asyncio(loop_scope="function")
31-
async def test_request_preparation(self, api_client_integration: ManagementAPIClient):
27+
# Create client but don't mock the httpx client yet
28+
client = ManagementAPIClient(settings=mock_settings)
29+
30+
# Setup mock response
31+
mock_response = MagicMock(spec=httpx.Response)
32+
mock_response.status_code = 404
33+
mock_response.is_success = False
34+
mock_response.headers = {"content-type": "application/json"}
35+
mock_response.json.return_value = {"message": "Cannot GET /v1/health"}
36+
mock_response.text = '{"message": "Cannot GET /v1/health"}'
37+
mock_response.content = b'{"message": "Cannot GET /v1/health"}'
38+
39+
# Mock the send_request method to return our mock response
40+
with patch.object(client, 'send_request', return_value=mock_response):
41+
path = "/v1/health"
42+
43+
# Execute the request and expect a 404 error
44+
with pytest.raises(APIClientError) as exc_info:
45+
await client.execute_request(
46+
method="GET",
47+
path=path,
48+
)
49+
50+
# Verify the error details
51+
assert exc_info.value.status_code == 404
52+
assert "Cannot GET /v1/health" in str(exc_info.value)
53+
54+
async def test_request_preparation(self, mock_settings):
3255
"""Test that requests are properly prepared with headers and parameters."""
56+
client = ManagementAPIClient(settings=mock_settings)
57+
3358
# Prepare a request with parameters
3459
method = "GET"
3560
path = "/v1/health"
3661
request_params = {"param1": "value1", "param2": "value2"}
3762

3863
# Prepare the request
39-
request = api_client_integration.prepare_request(
64+
request = client.prepare_request(
4065
method=method,
4166
path=path,
4267
request_params=request_params,
@@ -50,37 +75,44 @@ async def test_request_preparation(self, api_client_integration: ManagementAPICl
5075
assert "Content-Type" in request.headers
5176
assert request.headers["Content-Type"] == "application/json"
5277

53-
# @pytest.mark.asyncio(loop_scope="function")
54-
async def test_error_handling(self, api_client_integration: ManagementAPIClient):
78+
async def test_error_handling(self, mock_settings):
5579
"""Test handling of API errors."""
56-
# Make a request to a non-existent endpoint
57-
path = "/v1/nonexistent-endpoint"
58-
59-
# Execute the request and expect an APIClientError (not APIResponseError)
60-
with pytest.raises(APIClientError) as exc_info:
61-
await api_client_integration.execute_request(
62-
method="GET",
63-
path=path,
64-
)
65-
66-
# Verify the error details
67-
assert exc_info.value.status_code == 404
68-
assert "Cannot GET /v1/nonexistent-endpoint" in str(exc_info.value)
69-
70-
# @pytest.mark.asyncio(loop_scope="function")
71-
async def test_request_with_body(self, api_client_integration: ManagementAPIClient):
80+
client = ManagementAPIClient(settings=mock_settings)
81+
82+
# Setup mock response
83+
mock_response = MagicMock(spec=httpx.Response)
84+
mock_response.status_code = 404
85+
mock_response.is_success = False
86+
mock_response.headers = {"content-type": "application/json"}
87+
mock_response.json.return_value = {"message": "Cannot GET /v1/nonexistent-endpoint"}
88+
mock_response.text = '{"message": "Cannot GET /v1/nonexistent-endpoint"}'
89+
mock_response.content = b'{"message": "Cannot GET /v1/nonexistent-endpoint"}'
90+
91+
with patch.object(client, 'send_request', return_value=mock_response):
92+
path = "/v1/nonexistent-endpoint"
93+
94+
# Execute the request and expect an APIClientError
95+
with pytest.raises(APIClientError) as exc_info:
96+
await client.execute_request(
97+
method="GET",
98+
path=path,
99+
)
100+
101+
# Verify the error details
102+
assert exc_info.value.status_code == 404
103+
assert "Cannot GET /v1/nonexistent-endpoint" in str(exc_info.value)
104+
105+
async def test_request_with_body(self, mock_settings):
72106
"""Test executing a request with a body."""
73-
# This test would normally make a POST request with a body
74-
# Since we don't want to create real resources, we'll use a mock
75-
# or a safe endpoint that accepts POST but doesn't modify anything
76-
77-
# For now, we'll just test the request preparation
107+
client = ManagementAPIClient(settings=mock_settings)
108+
109+
# Test the request preparation
78110
method = "POST"
79-
path = "/v1/health/check" # This is a hypothetical endpoint
111+
path = "/v1/health/check"
80112
request_body = {"test": "data", "nested": {"value": 123}}
81113

82114
# Prepare the request
83-
request = api_client_integration.prepare_request(
115+
request = client.prepare_request(
84116
method=method,
85117
path=path,
86118
request_body=request_body,
@@ -93,56 +125,64 @@ async def test_request_with_body(self, api_client_integration: ManagementAPIClie
93125
assert "Content-Type" in request.headers
94126
assert request.headers["Content-Type"] == "application/json"
95127

96-
# @pytest.mark.asyncio(loop_scope="function")
97-
async def test_response_parsing(self, api_client_integration: ManagementAPIClient):
128+
async def test_response_parsing(self, mock_settings):
98129
"""Test parsing API responses."""
99-
# Make a request to a public endpoint that returns JSON
100-
path = "/v1/projects"
101-
102-
# Execute the request
103-
response = await api_client_integration.execute_request(
104-
method="GET",
105-
path=path,
106-
)
107-
108-
# Verify the response is parsed correctly
109-
assert isinstance(response, list)
110-
# The response is a list of projects, not a dict with a "projects" key
111-
assert len(response) > 0
112-
assert "id" in response[0]
113-
114-
# @pytest.mark.asyncio(loop_scope="function")
115-
async def test_request_retry_mechanism(self, monkeypatch, api_client_integration: ManagementAPIClient):
116-
"""Test that the tenacity retry mechanism works correctly for API requests."""
117-
118-
# Create a simple mock that always raises a network error
119-
async def mock_send(*args, **kwargs):
120-
raise httpx.NetworkError("Simulated network failure", request=None)
121-
122-
# Replace the client's send method with our mock
123-
monkeypatch.setattr(api_client_integration.client, "send", mock_send)
124-
125-
# Execute a request - this should trigger retries and eventually fail
126-
with pytest.raises(APIConnectionError) as exc_info:
127-
await api_client_integration.execute_request(
130+
client = ManagementAPIClient(settings=mock_settings)
131+
132+
# Setup mock response
133+
mock_response = MagicMock(spec=httpx.Response)
134+
mock_response.status_code = 200
135+
mock_response.is_success = True
136+
mock_response.headers = {"content-type": "application/json"}
137+
mock_response.json.return_value = [{"id": "project1", "name": "Test Project"}]
138+
mock_response.content = b'[{"id": "project1", "name": "Test Project"}]'
139+
140+
with patch.object(client, 'send_request', return_value=mock_response):
141+
path = "/v1/projects"
142+
143+
# Execute the request
144+
response = await client.execute_request(
128145
method="GET",
129-
path="/v1/projects",
146+
path=path,
130147
)
148+
149+
# Verify the response is parsed correctly
150+
assert isinstance(response, list)
151+
assert len(response) > 0
152+
assert "id" in response[0]
131153

132-
# Verify the error message indicates retries were attempted
133-
assert "Network error after 3 retry attempts" in str(exc_info.value)
134-
135-
# @pytest.mark.asyncio(loop_scope="function")
136-
async def test_request_without_access_token(
137-
self, monkeypatch: pytest.MonkeyPatch, api_client_integration: ManagementAPIClient
138-
):
154+
async def test_request_retry_mechanism(self, mock_settings):
155+
"""Test that the tenacity retry mechanism works correctly for API requests."""
156+
client = ManagementAPIClient(settings=mock_settings)
157+
158+
# Create a mock request object for the NetworkError
159+
mock_request = MagicMock(spec=httpx.Request)
160+
mock_request.method = "GET"
161+
mock_request.url = "https://api.supabase.com/v1/projects"
162+
163+
# Mock the client's send method to always raise a network error
164+
with patch.object(client.client, 'send', side_effect=httpx.NetworkError("Simulated network failure", request=mock_request)):
165+
# Execute a request - this should trigger retries and eventually fail
166+
with pytest.raises(APIConnectionError) as exc_info:
167+
await client.execute_request(
168+
method="GET",
169+
path="/v1/projects",
170+
)
171+
172+
# Verify the error message indicates retries were attempted
173+
assert "Network error after 3 retry attempts" in str(exc_info.value)
174+
175+
async def test_request_without_access_token(self, mock_settings):
139176
"""Test that an exception is raised when attempting to send a request without an access token."""
140-
# Temporarily set the access token to None
141-
monkeypatch.setattr(api_client_integration.settings, "supabase_access_token", None)
177+
# Create client with no access token
178+
mock_settings.supabase_access_token = None
179+
client = ManagementAPIClient(settings=mock_settings)
142180

143181
# Attempt to execute a request - should raise an exception
144-
with pytest.raises(APIClientError):
145-
await api_client_integration.execute_request(
182+
with pytest.raises(APIClientError) as exc_info:
183+
await client.execute_request(
146184
method="GET",
147185
path="/v1/projects",
148186
)
187+
188+
assert "Supabase access token is not configured" in str(exc_info.value)

0 commit comments

Comments
 (0)