mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
- Test StackAuthError base class storage and string representation - Test all 13 category subclasses inherit from StackAuthError - Test from_response() dispatch for known and unknown error codes - Test StackAuthModel config and millis conversion - Package skeleton with pyproject.toml (stubs only, tests expected to fail)
188 lines
6.5 KiB
Python
188 lines
6.5 KiB
Python
"""Tests for the Stack Auth error hierarchy and from_response() dispatch."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
|
|
from stack_auth.errors import (
|
|
AnalyticsError,
|
|
ApiKeyError,
|
|
AuthenticationError,
|
|
CliError,
|
|
ConflictError,
|
|
EmailError,
|
|
NotFoundError,
|
|
OAuthError,
|
|
PasskeyError,
|
|
PaymentError,
|
|
PermissionDeniedError,
|
|
RateLimitError,
|
|
StackAuthError,
|
|
ValidationError,
|
|
)
|
|
from stack_auth.models._base import StackAuthModel
|
|
|
|
|
|
class TestStackAuthErrorBasics:
|
|
"""Test StackAuthError base class storage and string representation."""
|
|
|
|
def test_stores_code_and_message(self) -> None:
|
|
err = StackAuthError("CODE", "msg")
|
|
assert err.code == "CODE"
|
|
assert err.message == "msg"
|
|
|
|
def test_str_contains_code_and_message(self) -> None:
|
|
err = StackAuthError("CODE", "msg")
|
|
s = str(err)
|
|
assert "CODE" in s
|
|
assert "msg" in s
|
|
|
|
def test_stores_details(self) -> None:
|
|
err = StackAuthError("CODE", "msg", {"key": "val"})
|
|
assert err.details == {"key": "val"}
|
|
|
|
def test_details_default_none(self) -> None:
|
|
err = StackAuthError("CODE", "msg")
|
|
assert err.details is None
|
|
|
|
|
|
class TestErrorSubclasses:
|
|
"""Test that all category subclasses exist and inherit from StackAuthError."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"cls",
|
|
[
|
|
AuthenticationError,
|
|
NotFoundError,
|
|
ValidationError,
|
|
PermissionDeniedError,
|
|
ConflictError,
|
|
OAuthError,
|
|
PasskeyError,
|
|
ApiKeyError,
|
|
PaymentError,
|
|
EmailError,
|
|
RateLimitError,
|
|
CliError,
|
|
AnalyticsError,
|
|
],
|
|
)
|
|
def test_is_subclass_of_stack_auth_error(self, cls: type) -> None:
|
|
assert issubclass(cls, StackAuthError)
|
|
|
|
@pytest.mark.parametrize(
|
|
"cls",
|
|
[
|
|
AuthenticationError,
|
|
NotFoundError,
|
|
ValidationError,
|
|
PermissionDeniedError,
|
|
ConflictError,
|
|
OAuthError,
|
|
PasskeyError,
|
|
ApiKeyError,
|
|
PaymentError,
|
|
EmailError,
|
|
RateLimitError,
|
|
CliError,
|
|
AnalyticsError,
|
|
],
|
|
)
|
|
def test_can_be_caught_with_except_stack_auth_error(self, cls: type) -> None:
|
|
with pytest.raises(StackAuthError):
|
|
raise cls("TEST_CODE", "test message")
|
|
|
|
|
|
class TestFromResponse:
|
|
"""Test from_response() factory dispatch for known and unknown codes."""
|
|
|
|
def test_invalid_access_token_returns_authentication_error(self) -> None:
|
|
err = StackAuthError.from_response("INVALID_ACCESS_TOKEN", "bad token")
|
|
assert isinstance(err, AuthenticationError)
|
|
assert err.code == "INVALID_ACCESS_TOKEN"
|
|
assert err.message == "bad token"
|
|
|
|
def test_user_not_found_returns_not_found_error(self) -> None:
|
|
err = StackAuthError.from_response("USER_NOT_FOUND", "no user")
|
|
assert isinstance(err, NotFoundError)
|
|
|
|
def test_schema_error_returns_validation_error(self) -> None:
|
|
err = StackAuthError.from_response("SCHEMA_ERROR", "bad schema")
|
|
assert isinstance(err, ValidationError)
|
|
|
|
def test_team_permission_required_returns_permission_error(self) -> None:
|
|
err = StackAuthError.from_response("TEAM_PERMISSION_REQUIRED", "denied")
|
|
assert isinstance(err, PermissionDeniedError)
|
|
|
|
def test_team_already_exists_returns_conflict_error(self) -> None:
|
|
err = StackAuthError.from_response("TEAM_ALREADY_EXISTS", "conflict")
|
|
assert isinstance(err, ConflictError)
|
|
|
|
def test_oauth_connection_not_connected_returns_oauth_error(self) -> None:
|
|
err = StackAuthError.from_response("OAUTH_CONNECTION_NOT_CONNECTED_TO_USER", "no conn")
|
|
assert isinstance(err, OAuthError)
|
|
|
|
def test_passkey_authentication_failed_returns_passkey_error(self) -> None:
|
|
err = StackAuthError.from_response("PASSKEY_AUTHENTICATION_FAILED", "failed")
|
|
assert isinstance(err, PasskeyError)
|
|
|
|
def test_api_key_not_valid_returns_api_key_error(self) -> None:
|
|
err = StackAuthError.from_response("API_KEY_NOT_VALID", "invalid")
|
|
assert isinstance(err, ApiKeyError)
|
|
|
|
def test_product_already_granted_returns_payment_error(self) -> None:
|
|
err = StackAuthError.from_response("PRODUCT_ALREADY_GRANTED", "granted")
|
|
assert isinstance(err, PaymentError)
|
|
|
|
def test_email_rendering_error_returns_email_error(self) -> None:
|
|
err = StackAuthError.from_response("EMAIL_RENDERING_ERROR", "render fail")
|
|
assert isinstance(err, EmailError)
|
|
|
|
def test_unknown_code_returns_base_stack_auth_error(self) -> None:
|
|
err = StackAuthError.from_response("TOTALLY_UNKNOWN_CODE", "surprise")
|
|
assert type(err) is StackAuthError
|
|
assert err.code == "TOTALLY_UNKNOWN_CODE"
|
|
assert err.message == "surprise"
|
|
|
|
def test_from_response_preserves_details(self) -> None:
|
|
err = StackAuthError.from_response("USER_NOT_FOUND", "no user", {"user_id": "abc"})
|
|
assert err.details == {"user_id": "abc"}
|
|
|
|
def test_cli_auth_error_returns_cli_error(self) -> None:
|
|
err = StackAuthError.from_response("CLI_AUTH_ERROR", "cli fail")
|
|
assert isinstance(err, CliError)
|
|
|
|
def test_analytics_query_timeout_returns_analytics_error(self) -> None:
|
|
err = StackAuthError.from_response("ANALYTICS_QUERY_TIMEOUT", "timeout")
|
|
assert isinstance(err, AnalyticsError)
|
|
|
|
|
|
class TestErrorCodeMapCoverage:
|
|
"""Test that the error code map has comprehensive coverage."""
|
|
|
|
def test_error_code_map_has_at_least_90_entries(self) -> None:
|
|
from stack_auth.errors import _ERROR_CODE_MAP
|
|
|
|
assert len(_ERROR_CODE_MAP) >= 90, f"Expected >= 90 entries, got {len(_ERROR_CODE_MAP)}"
|
|
|
|
|
|
class TestStackAuthModel:
|
|
"""Test the StackAuthModel base class."""
|
|
|
|
def test_model_config_populate_by_name(self) -> None:
|
|
assert StackAuthModel.model_config.get("populate_by_name") is True
|
|
|
|
def test_model_config_extra_ignore(self) -> None:
|
|
assert StackAuthModel.model_config.get("extra") == "ignore"
|
|
|
|
def test_millis_to_datetime_converts_correctly(self) -> None:
|
|
result = StackAuthModel._millis_to_datetime(1711296000000)
|
|
expected = datetime(2024, 3, 24, 16, 0, tzinfo=timezone.utc)
|
|
assert result == expected
|
|
|
|
def test_millis_to_datetime_none_returns_none(self) -> None:
|
|
result = StackAuthModel._millis_to_datetime(None)
|
|
assert result is None
|