Merge pull request 'fix: add trailing slashes to prevent 307 redirects dropping POST bodies' (#73) from fix/trailing-slash-307-redirect into main
All checks were successful
Build Docker Image / build (push) Successful in 53s

Reviewed-on: #73
This commit is contained in:
cal 2026-03-09 22:26:52 +00:00
commit 52ab92523c
2 changed files with 183 additions and 186 deletions

View File

@ -88,6 +88,10 @@ class APIClient:
encoded_id = quote(str(object_id), safe="") encoded_id = quote(str(object_id), safe="")
path += f"/{encoded_id}" path += f"/{encoded_id}"
# Ensure trailing slash to prevent 307 redirects that drop POST bodies
if not path.endswith("/"):
path += "/"
return urljoin(self.base_url.rstrip("/") + "/", path) return urljoin(self.base_url.rstrip("/") + "/", path)
def _add_params(self, url: str, params: Optional[List[tuple]] = None) -> str: def _add_params(self, url: str, params: Optional[List[tuple]] = None) -> str:

View File

@ -1,18 +1,24 @@
""" """
API client tests using aioresponses for clean HTTP mocking API client tests using aioresponses for clean HTTP mocking
""" """
import pytest import pytest
import asyncio import asyncio
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from aioresponses import aioresponses from aioresponses import aioresponses
from api.client import APIClient, get_api_client, get_global_client, cleanup_global_client from api.client import (
APIClient,
get_api_client,
get_global_client,
cleanup_global_client,
)
from exceptions import APIException from exceptions import APIException
class TestAPIClientWithAioresponses: class TestAPIClientWithAioresponses:
"""Test API client with aioresponses for HTTP mocking.""" """Test API client with aioresponses for HTTP mocking."""
@pytest.fixture @pytest.fixture
def mock_config(self): def mock_config(self):
"""Mock configuration for testing.""" """Mock configuration for testing."""
@ -20,202 +26,185 @@ class TestAPIClientWithAioresponses:
config.db_url = "https://api.example.com" config.db_url = "https://api.example.com"
config.api_token = "test-token" config.api_token = "test-token"
return config return config
@pytest.fixture @pytest.fixture
def api_client(self, mock_config): def api_client(self, mock_config):
"""Create API client with mocked config.""" """Create API client with mocked config."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
return APIClient() return APIClient()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_request_success(self, api_client): async def test_get_request_success(self, api_client):
"""Test successful GET request.""" """Test successful GET request."""
expected_data = {"id": 1, "name": "Test Player"} expected_data = {"id": 1, "name": "Test Player"}
with aioresponses() as m: with aioresponses() as m:
m.get( m.get(
"https://api.example.com/v3/players/1", "https://api.example.com/v3/players/1/",
payload=expected_data, payload=expected_data,
status=200 status=200,
) )
result = await api_client.get("players", object_id=1) result = await api_client.get("players", object_id=1)
assert result == expected_data assert result == expected_data
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_request_404(self, api_client): async def test_get_request_404(self, api_client):
"""Test GET request returning 404.""" """Test GET request returning 404."""
with aioresponses() as m: with aioresponses() as m:
m.get( m.get("https://api.example.com/v3/players/999/", status=404)
"https://api.example.com/v3/players/999",
status=404
)
result = await api_client.get("players", object_id=999) result = await api_client.get("players", object_id=999)
assert result is None assert result is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_request_401_auth_error(self, api_client): async def test_get_request_401_auth_error(self, api_client):
"""Test GET request with authentication error.""" """Test GET request with authentication error."""
with aioresponses() as m: with aioresponses() as m:
m.get( m.get("https://api.example.com/v3/players/", status=401)
"https://api.example.com/v3/players",
status=401
)
with pytest.raises(APIException, match="Authentication failed"): with pytest.raises(APIException, match="Authentication failed"):
await api_client.get("players") await api_client.get("players")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_request_403_forbidden(self, api_client): async def test_get_request_403_forbidden(self, api_client):
"""Test GET request with forbidden error.""" """Test GET request with forbidden error."""
with aioresponses() as m: with aioresponses() as m:
m.get( m.get("https://api.example.com/v3/players/", status=403)
"https://api.example.com/v3/players",
status=403
)
with pytest.raises(APIException, match="Access forbidden"): with pytest.raises(APIException, match="Access forbidden"):
await api_client.get("players") await api_client.get("players")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_request_500_server_error(self, api_client): async def test_get_request_500_server_error(self, api_client):
"""Test GET request with server error.""" """Test GET request with server error."""
with aioresponses() as m: with aioresponses() as m:
m.get( m.get(
"https://api.example.com/v3/players", "https://api.example.com/v3/players/",
status=500, status=500,
body="Internal Server Error" body="Internal Server Error",
) )
with pytest.raises(APIException, match="API request failed with status 500"): with pytest.raises(
APIException, match="API request failed with status 500"
):
await api_client.get("players") await api_client.get("players")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_request_with_params(self, api_client): async def test_get_request_with_params(self, api_client):
"""Test GET request with query parameters.""" """Test GET request with query parameters."""
expected_data = {"count": 2, "players": [{"id": 1}, {"id": 2}]} expected_data = {"count": 2, "players": [{"id": 1}, {"id": 2}]}
with aioresponses() as m: with aioresponses() as m:
m.get( m.get(
"https://api.example.com/v3/players?team_id=5&season=12", "https://api.example.com/v3/players/?team_id=5&season=12",
payload=expected_data, payload=expected_data,
status=200 status=200,
) )
result = await api_client.get("players", params=[("team_id", "5"), ("season", "12")]) result = await api_client.get(
"players", params=[("team_id", "5"), ("season", "12")]
)
assert result == expected_data assert result == expected_data
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_post_request_success(self, api_client): async def test_post_request_success(self, api_client):
"""Test successful POST request.""" """Test successful POST request."""
input_data = {"name": "New Player", "position": "C"} input_data = {"name": "New Player", "position": "C"}
expected_response = {"id": 1, "name": "New Player", "position": "C"} expected_response = {"id": 1, "name": "New Player", "position": "C"}
with aioresponses() as m: with aioresponses() as m:
m.post( m.post(
"https://api.example.com/v3/players", "https://api.example.com/v3/players/",
payload=expected_response, payload=expected_response,
status=201 status=201,
) )
result = await api_client.post("players", input_data) result = await api_client.post("players", input_data)
assert result == expected_response assert result == expected_response
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_post_request_400_error(self, api_client): async def test_post_request_400_error(self, api_client):
"""Test POST request with validation error.""" """Test POST request with validation error."""
input_data = {"invalid": "data"} input_data = {"invalid": "data"}
with aioresponses() as m: with aioresponses() as m:
m.post( m.post(
"https://api.example.com/v3/players", "https://api.example.com/v3/players/", status=400, body="Invalid data"
status=400,
body="Invalid data"
) )
with pytest.raises(APIException, match="POST request failed with status 400"): with pytest.raises(
APIException, match="POST request failed with status 400"
):
await api_client.post("players", input_data) await api_client.post("players", input_data)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_put_request_success(self, api_client): async def test_put_request_success(self, api_client):
"""Test successful PUT request.""" """Test successful PUT request."""
update_data = {"name": "Updated Player"} update_data = {"name": "Updated Player"}
expected_response = {"id": 1, "name": "Updated Player"} expected_response = {"id": 1, "name": "Updated Player"}
with aioresponses() as m: with aioresponses() as m:
m.put( m.put(
"https://api.example.com/v3/players/1", "https://api.example.com/v3/players/1/",
payload=expected_response, payload=expected_response,
status=200 status=200,
) )
result = await api_client.put("players", update_data, object_id=1) result = await api_client.put("players", update_data, object_id=1)
assert result == expected_response assert result == expected_response
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_put_request_404(self, api_client): async def test_put_request_404(self, api_client):
"""Test PUT request with 404.""" """Test PUT request with 404."""
update_data = {"name": "Updated Player"} update_data = {"name": "Updated Player"}
with aioresponses() as m: with aioresponses() as m:
m.put( m.put("https://api.example.com/v3/players/999/", status=404)
"https://api.example.com/v3/players/999",
status=404
)
result = await api_client.put("players", update_data, object_id=999) result = await api_client.put("players", update_data, object_id=999)
assert result is None assert result is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_delete_request_success(self, api_client): async def test_delete_request_success(self, api_client):
"""Test successful DELETE request.""" """Test successful DELETE request."""
with aioresponses() as m: with aioresponses() as m:
m.delete( m.delete("https://api.example.com/v3/players/1/", status=204)
"https://api.example.com/v3/players/1",
status=204
)
result = await api_client.delete("players", object_id=1) result = await api_client.delete("players", object_id=1)
assert result is True assert result is True
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_delete_request_404(self, api_client): async def test_delete_request_404(self, api_client):
"""Test DELETE request with 404.""" """Test DELETE request with 404."""
with aioresponses() as m: with aioresponses() as m:
m.delete( m.delete("https://api.example.com/v3/players/999/", status=404)
"https://api.example.com/v3/players/999",
status=404
)
result = await api_client.delete("players", object_id=999) result = await api_client.delete("players", object_id=999)
assert result is False assert result is False
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_delete_request_200_success(self, api_client): async def test_delete_request_200_success(self, api_client):
"""Test DELETE request with 200 success.""" """Test DELETE request with 200 success."""
with aioresponses() as m: with aioresponses() as m:
m.delete( m.delete("https://api.example.com/v3/players/1/", status=200)
"https://api.example.com/v3/players/1",
status=200
)
result = await api_client.delete("players", object_id=1) result = await api_client.delete("players", object_id=1)
assert result is True assert result is True
class TestAPIClientHelpers: class TestAPIClientHelpers:
"""Test API client helper functions.""" """Test API client helper functions."""
@pytest.fixture @pytest.fixture
def mock_config(self): def mock_config(self):
"""Mock configuration for testing.""" """Mock configuration for testing."""
@ -223,49 +212,49 @@ class TestAPIClientHelpers:
config.db_url = "https://api.example.com" config.db_url = "https://api.example.com"
config.api_token = "test-token" config.api_token = "test-token"
return config return config
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_api_client_context_manager(self, mock_config): async def test_get_api_client_context_manager(self, mock_config):
"""Test get_api_client context manager.""" """Test get_api_client context manager."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
with aioresponses() as m: with aioresponses() as m:
m.get( m.get(
"https://api.example.com/v3/test", "https://api.example.com/v3/test/",
payload={"success": True}, payload={"success": True},
status=200 status=200,
) )
async with get_api_client() as client: async with get_api_client() as client:
assert isinstance(client, APIClient) assert isinstance(client, APIClient)
result = await client.get("test") result = await client.get("test")
assert result == {"success": True} assert result == {"success": True}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_global_client_management(self, mock_config): async def test_global_client_management(self, mock_config):
"""Test global client getter and cleanup.""" """Test global client getter and cleanup."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
# Get global client # Get global client
client1 = await get_global_client() client1 = await get_global_client()
client2 = await get_global_client() client2 = await get_global_client()
# Should return same instance # Should return same instance
assert client1 is client2 assert client1 is client2
assert isinstance(client1, APIClient) assert isinstance(client1, APIClient)
# Test cleanup # Test cleanup
await cleanup_global_client() await cleanup_global_client()
# New client should be different instance # New client should be different instance
client3 = await get_global_client() client3 = await get_global_client()
assert client3 is not client1 assert client3 is not client1
# Clean up for other tests # Clean up for other tests
await cleanup_global_client() await cleanup_global_client()
class TestIntegrationScenarios: class TestIntegrationScenarios:
"""Test realistic integration scenarios.""" """Test realistic integration scenarios."""
@pytest.fixture @pytest.fixture
def mock_config(self): def mock_config(self):
"""Mock configuration for testing.""" """Mock configuration for testing."""
@ -273,11 +262,11 @@ class TestIntegrationScenarios:
config.db_url = "https://api.example.com" config.db_url = "https://api.example.com"
config.api_token = "test-token" config.api_token = "test-token"
return config return config
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_player_retrieval_with_team_lookup(self, mock_config): async def test_player_retrieval_with_team_lookup(self, mock_config):
"""Test realistic scenario: get player with team data.""" """Test realistic scenario: get player with team data."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
with aioresponses() as m: with aioresponses() as m:
# Mock player data response # Mock player data response
player_data = { player_data = {
@ -287,43 +276,41 @@ class TestIntegrationScenarios:
"season": 12, "season": 12,
"team_id": 5, "team_id": 5,
"image": "https://example.com/player1.jpg", "image": "https://example.com/player1.jpg",
"pos_1": "C" "pos_1": "C",
} }
m.get( m.get(
"https://api.example.com/v3/players/1", "https://api.example.com/v3/players/1/",
payload=player_data, payload=player_data,
status=200 status=200,
) )
# Mock team data response # Mock team data response
team_data = { team_data = {
"id": 5, "id": 5,
"abbrev": "TST", "abbrev": "TST",
"sname": "Test Team", "sname": "Test Team",
"lname": "Test Team Full Name", "lname": "Test Team Full Name",
"season": 12 "season": 12,
} }
m.get( m.get(
"https://api.example.com/v3/teams/5", "https://api.example.com/v3/teams/5/", payload=team_data, status=200
payload=team_data,
status=200
) )
client = APIClient() client = APIClient()
# Get player # Get player
player = await client.get("players", object_id=1) player = await client.get("players", object_id=1)
assert player["name"] == "Test Player" assert player["name"] == "Test Player"
assert player["team_id"] == 5 assert player["team_id"] == 5
# Get team for player # Get team for player
team = await client.get("teams", object_id=player["team_id"]) team = await client.get("teams", object_id=player["team_id"])
assert team["sname"] == "Test Team" assert team["sname"] == "Test Team"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_api_response_format_handling(self, mock_config): async def test_api_response_format_handling(self, mock_config):
"""Test handling of the API's count + list format.""" """Test handling of the API's count + list format."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
with aioresponses() as m: with aioresponses() as m:
# Mock API response with count format # Mock API response with count format
api_response = { api_response = {
@ -336,7 +323,7 @@ class TestIntegrationScenarios:
"season": 12, "season": 12,
"team_id": 5, "team_id": 5,
"image": "https://example.com/player1.jpg", "image": "https://example.com/player1.jpg",
"pos_1": "C" "pos_1": "C",
}, },
{ {
"id": 2, "id": 2,
@ -345,93 +332,93 @@ class TestIntegrationScenarios:
"season": 12, "season": 12,
"team_id": 6, "team_id": 6,
"image": "https://example.com/player2.jpg", "image": "https://example.com/player2.jpg",
"pos_1": "1B" "pos_1": "1B",
} },
] ],
} }
m.get( m.get(
"https://api.example.com/v3/players?team_id=5", "https://api.example.com/v3/players/?team_id=5",
payload=api_response, payload=api_response,
status=200 status=200,
) )
client = APIClient() client = APIClient()
result = await client.get("players", params=[("team_id", "5")]) result = await client.get("players", params=[("team_id", "5")])
assert result["count"] == 25 assert result["count"] == 25
assert len(result["players"]) == 2 assert len(result["players"]) == 2
assert result["players"][0]["name"] == "Player 1" assert result["players"][0]["name"] == "Player 1"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_error_recovery_scenarios(self, mock_config): async def test_error_recovery_scenarios(self, mock_config):
"""Test error handling and recovery.""" """Test error handling and recovery."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
with aioresponses() as m: with aioresponses() as m:
# First request fails with 500 # First request fails with 500
m.get( m.get(
"https://api.example.com/v3/players/1", "https://api.example.com/v3/players/1/",
status=500, status=500,
body="Internal Server Error" body="Internal Server Error",
) )
# Second request succeeds # Second request succeeds
m.get( m.get(
"https://api.example.com/v3/players/2", "https://api.example.com/v3/players/2/",
payload={"id": 2, "name": "Working Player"}, payload={"id": 2, "name": "Working Player"},
status=200 status=200,
) )
client = APIClient() client = APIClient()
# First request should raise exception # First request should raise exception
with pytest.raises(APIException, match="API request failed"): with pytest.raises(APIException, match="API request failed"):
await client.get("players", object_id=1) await client.get("players", object_id=1)
# Second request should work fine # Second request should work fine
result = await client.get("players", object_id=2) result = await client.get("players", object_id=2)
assert result["name"] == "Working Player" assert result["name"] == "Working Player"
# Client should still be functional # Client should still be functional
await client.close() await client.close()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_concurrent_requests(self, mock_config): async def test_concurrent_requests(self, mock_config):
"""Test multiple concurrent requests.""" """Test multiple concurrent requests."""
import asyncio import asyncio
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
with aioresponses() as m: with aioresponses() as m:
# Mock multiple endpoints # Mock multiple endpoints
for i in range(1, 4): for i in range(1, 4):
m.get( m.get(
f"https://api.example.com/v3/players/{i}", f"https://api.example.com/v3/players/{i}/",
payload={"id": i, "name": f"Player {i}"}, payload={"id": i, "name": f"Player {i}"},
status=200 status=200,
) )
client = APIClient() client = APIClient()
# Make concurrent requests # Make concurrent requests
tasks = [ tasks = [
client.get("players", object_id=1), client.get("players", object_id=1),
client.get("players", object_id=2), client.get("players", object_id=2),
client.get("players", object_id=3) client.get("players", object_id=3),
] ]
results = await asyncio.gather(*tasks) results = await asyncio.gather(*tasks)
assert len(results) == 3 assert len(results) == 3
assert results[0]["name"] == "Player 1" assert results[0]["name"] == "Player 1"
assert results[1]["name"] == "Player 2" assert results[1]["name"] == "Player 2"
assert results[2]["name"] == "Player 3" assert results[2]["name"] == "Player 3"
await client.close() await client.close()
class TestAPIClientCoverageExtras: class TestAPIClientCoverageExtras:
"""Additional coverage tests for API client edge cases.""" """Additional coverage tests for API client edge cases."""
@pytest.fixture @pytest.fixture
def mock_config(self): def mock_config(self):
"""Mock configuration for testing.""" """Mock configuration for testing."""
@ -439,98 +426,104 @@ class TestAPIClientCoverageExtras:
config.db_url = "https://api.example.com" config.db_url = "https://api.example.com"
config.api_token = "test-token" config.api_token = "test-token"
return config return config
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_global_client_cleanup_when_none(self): async def test_global_client_cleanup_when_none(self):
"""Test cleanup when no global client exists.""" """Test cleanup when no global client exists."""
# Ensure no global client exists # Ensure no global client exists
await cleanup_global_client() await cleanup_global_client()
# Should not raise error # Should not raise error
await cleanup_global_client() await cleanup_global_client()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_url_building_edge_cases(self, mock_config): async def test_url_building_edge_cases(self, mock_config):
"""Test URL building with various edge cases.""" """Test URL building with various edge cases."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
client = APIClient() client = APIClient()
# Test trailing slash handling # Test trailing slash handling
client.base_url = "https://api.example.com/" client.base_url = "https://api.example.com/"
url = client._build_url("players") url = client._build_url("players")
assert url == "https://api.example.com/v3/players" assert url == "https://api.example.com/v3/players/"
assert "//" not in url.replace("https://", "") assert "//" not in url.replace("https://", "").replace("//", "")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_parameter_handling_edge_cases(self, mock_config): async def test_parameter_handling_edge_cases(self, mock_config):
"""Test parameter handling with various scenarios.""" """Test parameter handling with various scenarios."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
client = APIClient() client = APIClient()
# Test with existing query string # Test with existing query string
url = client._add_params("https://example.com/api?existing=true", [("new", "param")]) url = client._add_params(
"https://example.com/api?existing=true", [("new", "param")]
)
assert url == "https://example.com/api?existing=true&new=param" assert url == "https://example.com/api?existing=true&new=param"
# Test with no parameters # Test with no parameters
url = client._add_params("https://example.com/api") url = client._add_params("https://example.com/api")
assert url == "https://example.com/api" assert url == "https://example.com/api"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_timeout_error_handling(self, mock_config): async def test_timeout_error_handling(self, mock_config):
"""Test timeout error handling using aioresponses.""" """Test timeout error handling using aioresponses."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
client = APIClient() client = APIClient()
# Test timeout using aioresponses exception parameter # Test timeout using aioresponses exception parameter
with aioresponses() as m: with aioresponses() as m:
m.get( m.get(
"https://api.example.com/v3/players", "https://api.example.com/v3/players/",
exception=asyncio.TimeoutError("Request timed out") exception=asyncio.TimeoutError("Request timed out"),
) )
with pytest.raises(APIException, match="API call failed.*Request timed out"): with pytest.raises(
APIException, match="API call failed.*Request timed out"
):
await client.get("players") await client.get("players")
await client.close() await client.close()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_generic_exception_handling(self, mock_config): async def test_generic_exception_handling(self, mock_config):
"""Test generic exception handling.""" """Test generic exception handling."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
client = APIClient() client = APIClient()
# Test generic exception # Test generic exception
with aioresponses() as m: with aioresponses() as m:
m.get( m.get(
"https://api.example.com/v3/players", "https://api.example.com/v3/players/",
exception=Exception("Generic error") exception=Exception("Generic error"),
) )
with pytest.raises(APIException, match="API call failed.*Generic error"): with pytest.raises(
APIException, match="API call failed.*Generic error"
):
await client.get("players") await client.get("players")
await client.close() await client.close()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_session_closed_handling(self, mock_config): async def test_session_closed_handling(self, mock_config):
"""Test handling of closed session.""" """Test handling of closed session."""
with patch('api.client.get_config', return_value=mock_config): with patch("api.client.get_config", return_value=mock_config):
# Test that the client recreates session when needed # Test that the client recreates session when needed
with aioresponses() as m: with aioresponses() as m:
m.get( m.get(
"https://api.example.com/v3/players", "https://api.example.com/v3/players/",
payload={"success": True}, payload={"success": True},
status=200 status=200,
) )
client = APIClient() client = APIClient()
# Close the session manually # Close the session manually
await client._ensure_session() await client._ensure_session()
await client._session.close() await client._session.close()
# Client should recreate session and work fine # Client should recreate session and work fine
result = await client.get("players") result = await client.get("players")
assert result == {"success": True} assert result == {"success": True}
await client.close() await client.close()