Files
stack/sdks/implementations/python/tests/test_app.py
T
Ejiro Asiuwhu 3094338b42 test: add failing tests for team CRUD methods
- Tests for get_team, list_teams, create_team, update_team, delete_team
- Tests for get_team_by_api_key two-step lookup
- Async equivalents for all team CRUD operations
- Uses respx mocking pattern consistent with existing user tests
2026-03-25 01:22:09 +01:00

1031 lines
36 KiB
Python

"""Tests for StackServerApp and AsyncStackServerApp facade classes."""
from __future__ import annotations
import asyncio
import httpx
import pytest
import respx
from stack_auth._app import AsyncStackServerApp, StackServerApp
# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------
USER_JSON = {
"id": "user-123",
"displayName": "Alice",
"primaryEmail": "[email protected]",
"primaryEmailVerified": True,
"profileImageUrl": None,
"signedUpAtMillis": 1700000000000,
"lastActiveAtMillis": 1700001000000,
"clientMetadata": {},
"clientReadOnlyMetadata": {},
"serverMetadata": {"role": "admin"},
"hasPassword": True,
"otpAuthEnabled": False,
"passkeyAuthEnabled": False,
"isMultiFactorRequired": False,
"isAnonymous": False,
"isRestricted": False,
"restrictedReason": None,
}
USERS_LIST_JSON = {
"items": [USER_JSON],
"pagination": {"next_cursor": "cursor-abc"},
}
BASE_URL = "https://api.stack-auth.com"
API_PREFIX = f"{BASE_URL}/api/v1"
# ---------------------------------------------------------------------------
# StackServerApp - Construction
# ---------------------------------------------------------------------------
class TestStackServerAppConstruction:
def test_constructs_with_required_params(self) -> None:
app = StackServerApp(project_id="proj", secret_server_key="sk")
assert app is not None
def test_constructs_with_optional_params(self) -> None:
app = StackServerApp(
project_id="proj",
secret_server_key="sk",
base_url="https://custom.example.com",
token_store="memory",
)
assert app is not None
def test_context_manager(self) -> None:
with StackServerApp(project_id="proj", secret_server_key="sk") as app:
assert app is not None
# ---------------------------------------------------------------------------
# StackServerApp - get_user
# ---------------------------------------------------------------------------
class TestGetUser:
@respx.mock
def test_get_user_success(self) -> None:
respx.get(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
user = app.get_user("user-123")
assert user is not None
assert user.id == "user-123"
assert user.display_name == "Alice"
assert user.server_metadata == {"role": "admin"}
@respx.mock
def test_get_user_not_found(self) -> None:
respx.get(f"{API_PREFIX}/users/nonexistent").mock(
return_value=httpx.Response(
200,
json={"message": "User not found"},
headers={
"x-stack-known-error": "USER_NOT_FOUND",
"x-stack-actual-status": "404",
},
)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
user = app.get_user("nonexistent")
assert user is None
# ---------------------------------------------------------------------------
# StackServerApp - list_users
# ---------------------------------------------------------------------------
class TestListUsers:
@respx.mock
def test_list_users_basic(self) -> None:
respx.get(f"{API_PREFIX}/users").mock(
return_value=httpx.Response(200, json=USERS_LIST_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
result = app.list_users()
assert len(result.items) == 1
assert result.items[0].id == "user-123"
assert result.next_cursor == "cursor-abc"
assert result.has_next_page is True
@respx.mock
def test_list_users_with_params(self) -> None:
route = respx.get(f"{API_PREFIX}/users").mock(
return_value=httpx.Response(
200, json={"items": [], "pagination": {}}
)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
result = app.list_users(
cursor="abc",
limit=10,
order_by="created_at",
desc=True,
query="search",
include_restricted=True,
include_anonymous=False,
)
assert len(result.items) == 0
# Verify query params were sent
request = route.calls[0].request
assert "cursor" in dict(request.url.params)
assert request.url.params["cursor"] == "abc"
assert request.url.params["limit"] == "10"
# ---------------------------------------------------------------------------
# StackServerApp - create_user
# ---------------------------------------------------------------------------
class TestCreateUser:
@respx.mock
def test_create_user_full(self) -> None:
respx.post(f"{API_PREFIX}/users").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
user = app.create_user(
primary_email="[email protected]",
password="secret123",
display_name="Alice",
)
assert user.id == "user-123"
assert user.primary_email == "[email protected]"
@respx.mock
def test_create_user_omits_none_fields(self) -> None:
route = respx.post(f"{API_PREFIX}/users").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
app.create_user(primary_email="[email protected]")
request = route.calls[0].request
import json
body = json.loads(request.content)
assert "primary_email" in body
assert "password" not in body
assert "display_name" not in body
# ---------------------------------------------------------------------------
# StackServerApp - update_user
# ---------------------------------------------------------------------------
class TestUpdateUser:
@respx.mock
def test_update_user_sends_only_provided_fields(self) -> None:
route = respx.patch(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
user = app.update_user("user-123", display_name="New Name")
assert user.id == "user-123"
import json
body = json.loads(route.calls[0].request.content)
assert body == {"display_name": "New Name"}
@respx.mock
def test_update_user_sends_none_to_clear_field(self) -> None:
route = respx.patch(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
app.update_user("user-123", display_name=None)
import json
body = json.loads(route.calls[0].request.content)
assert body == {"display_name": None}
@respx.mock
def test_update_user_sentinel_distinguishes_not_provided(self) -> None:
route = respx.patch(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
# Only provide display_name, not server_metadata
app.update_user("user-123", display_name="X")
import json
body = json.loads(route.calls[0].request.content)
assert "server_metadata" not in body
assert body == {"display_name": "X"}
# ---------------------------------------------------------------------------
# StackServerApp - delete_user
# ---------------------------------------------------------------------------
class TestDeleteUser:
@respx.mock
def test_delete_user(self) -> None:
respx.delete(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
result = app.delete_user("user-123")
assert result is None
# ---------------------------------------------------------------------------
# StackServerApp - get_user_by_api_key
# ---------------------------------------------------------------------------
class TestGetUserByApiKey:
@respx.mock
def test_get_user_by_api_key_success(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(200, json={"user_id": "user-123"})
)
respx.get(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
user = app.get_user_by_api_key("sk_abc")
assert user is not None
assert user.id == "user-123"
@respx.mock
def test_get_user_by_api_key_invalid(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(
200,
json={"message": "API key not valid"},
headers={
"x-stack-known-error": "API_KEY_NOT_VALID",
"x-stack-actual-status": "400",
},
)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
user = app.get_user_by_api_key("invalid")
assert user is None
@respx.mock
def test_get_user_by_api_key_no_user_id(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(200, json={"team_id": "team-1"})
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
user = app.get_user_by_api_key("sk_team")
assert user is None
# ---------------------------------------------------------------------------
# AsyncStackServerApp - Construction
# ---------------------------------------------------------------------------
class TestAsyncStackServerAppConstruction:
def test_constructs_with_required_params(self) -> None:
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
assert app is not None
@pytest.mark.asyncio
async def test_async_context_manager(self) -> None:
async with AsyncStackServerApp(
project_id="proj", secret_server_key="sk"
) as app:
assert app is not None
# ---------------------------------------------------------------------------
# AsyncStackServerApp - get_user
# ---------------------------------------------------------------------------
class TestAsyncGetUser:
@respx.mock
@pytest.mark.asyncio
async def test_get_user_success(self) -> None:
respx.get(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
user = await app.get_user("user-123")
assert user is not None
assert user.id == "user-123"
assert user.display_name == "Alice"
await app.aclose()
@respx.mock
@pytest.mark.asyncio
async def test_get_user_not_found(self) -> None:
respx.get(f"{API_PREFIX}/users/nonexistent").mock(
return_value=httpx.Response(
200,
json={"message": "User not found"},
headers={
"x-stack-known-error": "USER_NOT_FOUND",
"x-stack-actual-status": "404",
},
)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
user = await app.get_user("nonexistent")
assert user is None
await app.aclose()
# ---------------------------------------------------------------------------
# AsyncStackServerApp - list_users
# ---------------------------------------------------------------------------
class TestAsyncListUsers:
@respx.mock
@pytest.mark.asyncio
async def test_list_users_basic(self) -> None:
respx.get(f"{API_PREFIX}/users").mock(
return_value=httpx.Response(200, json=USERS_LIST_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
result = await app.list_users()
assert len(result.items) == 1
assert result.items[0].id == "user-123"
assert result.next_cursor == "cursor-abc"
await app.aclose()
# ---------------------------------------------------------------------------
# AsyncStackServerApp - create_user
# ---------------------------------------------------------------------------
class TestAsyncCreateUser:
@respx.mock
@pytest.mark.asyncio
async def test_create_user(self) -> None:
respx.post(f"{API_PREFIX}/users").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
user = await app.create_user(
primary_email="[email protected]",
display_name="Alice",
)
assert user.id == "user-123"
await app.aclose()
# ---------------------------------------------------------------------------
# AsyncStackServerApp - update_user
# ---------------------------------------------------------------------------
class TestAsyncUpdateUser:
@respx.mock
@pytest.mark.asyncio
async def test_update_user(self) -> None:
route = respx.patch(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
user = await app.update_user("user-123", display_name="New")
assert user.id == "user-123"
import json
body = json.loads(route.calls[0].request.content)
assert body == {"display_name": "New"}
await app.aclose()
# ---------------------------------------------------------------------------
# AsyncStackServerApp - delete_user
# ---------------------------------------------------------------------------
class TestAsyncDeleteUser:
@respx.mock
@pytest.mark.asyncio
async def test_delete_user(self) -> None:
respx.delete(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
result = await app.delete_user("user-123")
assert result is None
await app.aclose()
# ---------------------------------------------------------------------------
# AsyncStackServerApp - get_user_by_api_key
# ---------------------------------------------------------------------------
class TestAsyncGetUserByApiKey:
@respx.mock
@pytest.mark.asyncio
async def test_get_user_by_api_key_success(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(200, json={"user_id": "user-123"})
)
respx.get(f"{API_PREFIX}/users/user-123").mock(
return_value=httpx.Response(200, json=USER_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
user = await app.get_user_by_api_key("sk_abc")
assert user is not None
assert user.id == "user-123"
await app.aclose()
@respx.mock
@pytest.mark.asyncio
async def test_get_user_by_api_key_invalid(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(
200,
json={"message": "API key not valid"},
headers={
"x-stack-known-error": "API_KEY_NOT_VALID",
"x-stack-actual-status": "400",
},
)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
user = await app.get_user_by_api_key("invalid")
assert user is None
await app.aclose()
# ---------------------------------------------------------------------------
# Session test data
# ---------------------------------------------------------------------------
SESSION_JSON = {
"id": "sess-1",
"userId": "user-123",
"createdAtMillis": 1700000000000,
"isImpersonation": False,
"lastUsedAtMillis": 1700001000000,
"isCurrentSession": False,
"geoInfo": {
"city": "San Francisco",
"region": "CA",
"country": "US",
"countryName": "United States",
"latitude": 37.7749,
"longitude": -122.4194,
},
}
SESSION_JSON_2 = {
"id": "sess-2",
"userId": "user-123",
"createdAtMillis": 1700002000000,
"isImpersonation": False,
"lastUsedAtMillis": None,
"isCurrentSession": True,
"geoInfo": None,
}
SESSIONS_LIST_JSON = {"items": [SESSION_JSON, SESSION_JSON_2]}
# ---------------------------------------------------------------------------
# StackServerApp - list_sessions
# ---------------------------------------------------------------------------
class TestListSessions:
@respx.mock
def test_list_sessions(self) -> None:
respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json=SESSIONS_LIST_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
sessions = app.list_sessions("user-123")
assert len(sessions) == 2
assert sessions[0].id == "sess-1"
assert sessions[0].user_id == "user-123"
assert sessions[0].geo_info is not None
assert sessions[0].geo_info.city == "San Francisco"
assert sessions[1].id == "sess-2"
assert sessions[1].is_current_session is True
@respx.mock
def test_list_sessions_sends_user_id_param(self) -> None:
route = respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json={"items": []})
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
sessions = app.list_sessions("user-456")
assert sessions == []
request = route.calls[0].request
assert request.url.params["user_id"] == "user-456"
@respx.mock
def test_list_sessions_empty(self) -> None:
respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json={"items": []})
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
sessions = app.list_sessions("user-123")
assert sessions == []
# ---------------------------------------------------------------------------
# StackServerApp - get_session
# ---------------------------------------------------------------------------
class TestGetSession:
@respx.mock
def test_get_session_found(self) -> None:
respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json=SESSIONS_LIST_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
session = app.get_session("sess-1", user_id="user-123")
assert session is not None
assert session.id == "sess-1"
@respx.mock
def test_get_session_not_found(self) -> None:
respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json=SESSIONS_LIST_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
session = app.get_session("nonexistent", user_id="user-123")
assert session is None
# ---------------------------------------------------------------------------
# StackServerApp - revoke_session
# ---------------------------------------------------------------------------
class TestRevokeSession:
@respx.mock
def test_revoke_session(self) -> None:
route = respx.delete(f"{API_PREFIX}/auth/sessions/sess-1").mock(
return_value=httpx.Response(200)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
result = app.revoke_session("sess-1", user_id="user-123")
assert result is None
request = route.calls[0].request
assert request.url.params["user_id"] == "user-123"
# ---------------------------------------------------------------------------
# AsyncStackServerApp - list_sessions
# ---------------------------------------------------------------------------
class TestAsyncListSessions:
@respx.mock
@pytest.mark.asyncio
async def test_list_sessions(self) -> None:
respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json=SESSIONS_LIST_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
sessions = await app.list_sessions("user-123")
assert len(sessions) == 2
assert sessions[0].id == "sess-1"
assert sessions[1].id == "sess-2"
await app.aclose()
@respx.mock
@pytest.mark.asyncio
async def test_list_sessions_empty(self) -> None:
respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json={"items": []})
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
sessions = await app.list_sessions("user-123")
assert sessions == []
await app.aclose()
# ---------------------------------------------------------------------------
# AsyncStackServerApp - get_session
# ---------------------------------------------------------------------------
class TestAsyncGetSession:
@respx.mock
@pytest.mark.asyncio
async def test_get_session_found(self) -> None:
respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json=SESSIONS_LIST_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
session = await app.get_session("sess-1", user_id="user-123")
assert session is not None
assert session.id == "sess-1"
await app.aclose()
@respx.mock
@pytest.mark.asyncio
async def test_get_session_not_found(self) -> None:
respx.get(f"{API_PREFIX}/auth/sessions").mock(
return_value=httpx.Response(200, json=SESSIONS_LIST_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
session = await app.get_session("nonexistent", user_id="user-123")
assert session is None
await app.aclose()
# ---------------------------------------------------------------------------
# AsyncStackServerApp - revoke_session
# ---------------------------------------------------------------------------
class TestAsyncRevokeSession:
@respx.mock
@pytest.mark.asyncio
async def test_revoke_session(self) -> None:
route = respx.delete(f"{API_PREFIX}/auth/sessions/sess-1").mock(
return_value=httpx.Response(200)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
result = await app.revoke_session("sess-1", user_id="user-123")
assert result is None
request = route.calls[0].request
assert request.url.params["user_id"] == "user-123"
await app.aclose()
# ---------------------------------------------------------------------------
# Team test data
# ---------------------------------------------------------------------------
TEAM_JSON = {
"id": "team-123",
"displayName": "Engineering",
"profileImageUrl": None,
"clientMetadata": {},
"clientReadOnlyMetadata": {},
"serverMetadata": {"tier": "pro"},
"createdAtMillis": 1700000000000,
}
TEAM_JSON_2 = {
"id": "team-456",
"displayName": "Design",
"profileImageUrl": "https://img.example.com/design.png",
"clientMetadata": {"color": "blue"},
"clientReadOnlyMetadata": {},
"serverMetadata": {},
"createdAtMillis": 1700001000000,
}
TEAMS_LIST_JSON = {"items": [TEAM_JSON, TEAM_JSON_2]}
# ---------------------------------------------------------------------------
# StackServerApp - get_team
# ---------------------------------------------------------------------------
class TestGetTeam:
@respx.mock
def test_get_team_success(self) -> None:
respx.get(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
team = app.get_team("team-123")
assert team is not None
assert team.id == "team-123"
assert team.display_name == "Engineering"
assert team.server_metadata == {"tier": "pro"}
@respx.mock
def test_get_team_not_found(self) -> None:
respx.get(f"{API_PREFIX}/teams/nonexistent").mock(
return_value=httpx.Response(
200,
json={"message": "Team not found"},
headers={
"x-stack-known-error": "TEAM_NOT_FOUND",
"x-stack-actual-status": "404",
},
)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
team = app.get_team("nonexistent")
assert team is None
# ---------------------------------------------------------------------------
# StackServerApp - list_teams
# ---------------------------------------------------------------------------
class TestListTeams:
@respx.mock
def test_list_teams_basic(self) -> None:
respx.get(f"{API_PREFIX}/teams").mock(
return_value=httpx.Response(200, json=TEAMS_LIST_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
teams = app.list_teams()
assert len(teams) == 2
assert teams[0].id == "team-123"
assert teams[1].id == "team-456"
@respx.mock
def test_list_teams_with_user_id(self) -> None:
route = respx.get(f"{API_PREFIX}/teams").mock(
return_value=httpx.Response(200, json={"items": [TEAM_JSON]})
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
teams = app.list_teams(user_id="user-1")
assert len(teams) == 1
request = route.calls[0].request
assert request.url.params["user_id"] == "user-1"
# ---------------------------------------------------------------------------
# StackServerApp - create_team
# ---------------------------------------------------------------------------
class TestCreateTeam:
@respx.mock
def test_create_team_basic(self) -> None:
respx.post(f"{API_PREFIX}/teams").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
team = app.create_team(display_name="Engineering")
assert team.id == "team-123"
assert team.display_name == "Engineering"
@respx.mock
def test_create_team_with_optional_fields(self) -> None:
route = respx.post(f"{API_PREFIX}/teams").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
app.create_team(
display_name="Eng",
profile_image_url="https://img.example.com/eng.png",
creator_user_id="user-1",
)
import json
body = json.loads(route.calls[0].request.content)
assert body["display_name"] == "Eng"
assert body["profile_image_url"] == "https://img.example.com/eng.png"
assert body["creator_user_id"] == "user-1"
# ---------------------------------------------------------------------------
# StackServerApp - update_team
# ---------------------------------------------------------------------------
class TestUpdateTeam:
@respx.mock
def test_update_team_sends_only_provided_fields(self) -> None:
route = respx.patch(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
team = app.update_team("team-123", display_name="New Name")
assert team.id == "team-123"
import json
body = json.loads(route.calls[0].request.content)
assert body == {"display_name": "New Name"}
@respx.mock
def test_update_team_sends_none_to_clear_field(self) -> None:
route = respx.patch(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
app.update_team("team-123", profile_image_url=None)
import json
body = json.loads(route.calls[0].request.content)
assert body == {"profile_image_url": None}
@respx.mock
def test_update_team_unset_sentinel(self) -> None:
route = respx.patch(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
app.update_team("team-123", display_name="X")
import json
body = json.loads(route.calls[0].request.content)
assert "server_metadata" not in body
assert "client_metadata" not in body
# ---------------------------------------------------------------------------
# StackServerApp - delete_team
# ---------------------------------------------------------------------------
class TestDeleteTeam:
@respx.mock
def test_delete_team(self) -> None:
respx.delete(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
result = app.delete_team("team-123")
assert result is None
# ---------------------------------------------------------------------------
# StackServerApp - get_team_by_api_key
# ---------------------------------------------------------------------------
class TestGetTeamByApiKey:
@respx.mock
def test_get_team_by_api_key_success(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(200, json={"team_id": "team-123"})
)
respx.get(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
team = app.get_team_by_api_key("sk_team_abc")
assert team is not None
assert team.id == "team-123"
@respx.mock
def test_get_team_by_api_key_invalid(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(
200,
json={"message": "API key not valid"},
headers={
"x-stack-known-error": "API_KEY_NOT_VALID",
"x-stack-actual-status": "400",
},
)
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
team = app.get_team_by_api_key("invalid")
assert team is None
@respx.mock
def test_get_team_by_api_key_no_team_id(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(200, json={"user_id": "user-1"})
)
app = StackServerApp(project_id="proj", secret_server_key="sk")
team = app.get_team_by_api_key("sk_user_only")
assert team is None
# ---------------------------------------------------------------------------
# AsyncStackServerApp - Team CRUD
# ---------------------------------------------------------------------------
class TestAsyncGetTeam:
@respx.mock
@pytest.mark.asyncio
async def test_get_team_success(self) -> None:
respx.get(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
team = await app.get_team("team-123")
assert team is not None
assert team.id == "team-123"
assert team.display_name == "Engineering"
await app.aclose()
@respx.mock
@pytest.mark.asyncio
async def test_get_team_not_found(self) -> None:
respx.get(f"{API_PREFIX}/teams/nonexistent").mock(
return_value=httpx.Response(
200,
json={"message": "Team not found"},
headers={
"x-stack-known-error": "TEAM_NOT_FOUND",
"x-stack-actual-status": "404",
},
)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
team = await app.get_team("nonexistent")
assert team is None
await app.aclose()
class TestAsyncListTeams:
@respx.mock
@pytest.mark.asyncio
async def test_list_teams(self) -> None:
respx.get(f"{API_PREFIX}/teams").mock(
return_value=httpx.Response(200, json=TEAMS_LIST_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
teams = await app.list_teams()
assert len(teams) == 2
assert teams[0].id == "team-123"
await app.aclose()
class TestAsyncCreateTeam:
@respx.mock
@pytest.mark.asyncio
async def test_create_team(self) -> None:
respx.post(f"{API_PREFIX}/teams").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
team = await app.create_team(display_name="Engineering")
assert team.id == "team-123"
await app.aclose()
class TestAsyncUpdateTeam:
@respx.mock
@pytest.mark.asyncio
async def test_update_team(self) -> None:
route = respx.patch(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
team = await app.update_team("team-123", display_name="New")
assert team.id == "team-123"
import json
body = json.loads(route.calls[0].request.content)
assert body == {"display_name": "New"}
await app.aclose()
class TestAsyncDeleteTeam:
@respx.mock
@pytest.mark.asyncio
async def test_delete_team(self) -> None:
respx.delete(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
result = await app.delete_team("team-123")
assert result is None
await app.aclose()
class TestAsyncGetTeamByApiKey:
@respx.mock
@pytest.mark.asyncio
async def test_get_team_by_api_key_success(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(200, json={"team_id": "team-123"})
)
respx.get(f"{API_PREFIX}/teams/team-123").mock(
return_value=httpx.Response(200, json=TEAM_JSON)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
team = await app.get_team_by_api_key("sk_team_abc")
assert team is not None
assert team.id == "team-123"
await app.aclose()
@respx.mock
@pytest.mark.asyncio
async def test_get_team_by_api_key_invalid(self) -> None:
respx.post(f"{API_PREFIX}/api-keys/check").mock(
return_value=httpx.Response(
200,
json={"message": "API key not valid"},
headers={
"x-stack-known-error": "API_KEY_NOT_VALID",
"x-stack-actual-status": "400",
},
)
)
app = AsyncStackServerApp(project_id="proj", secret_server_key="sk")
team = await app.get_team_by_api_key("invalid")
assert team is None
await app.aclose()