pytest-clerk-mock 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ from pytest_clerk_mock.client import MockClerkClient
2
+ from pytest_clerk_mock.helpers import (
3
+ mock_clerk_user_creation,
4
+ mock_clerk_user_creation_failure,
5
+ mock_clerk_user_exists,
6
+ )
7
+ from pytest_clerk_mock.models.auth import MockAuthResult, MockClerkUser
8
+ from pytest_clerk_mock.models.organization import (
9
+ MockOrganization,
10
+ MockOrganizationMembership,
11
+ MockOrganizationMembershipsResponse,
12
+ )
13
+ from pytest_clerk_mock.models.user import MockEmailAddress, MockPhoneNumber, MockUser
14
+ from pytest_clerk_mock.plugin import (
15
+ create_mock_clerk_fixture,
16
+ mock_clerk,
17
+ mock_clerk_backend,
18
+ )
19
+ from pytest_clerk_mock.services.users import MockListResponse, UserNotFoundError
20
+
21
+ __all__ = [
22
+ "create_mock_clerk_fixture",
23
+ "mock_clerk",
24
+ "mock_clerk_backend",
25
+ "mock_clerk_user_creation",
26
+ "mock_clerk_user_creation_failure",
27
+ "mock_clerk_user_exists",
28
+ "MockAuthResult",
29
+ "MockClerkClient",
30
+ "MockClerkUser",
31
+ "MockEmailAddress",
32
+ "MockListResponse",
33
+ "MockOrganization",
34
+ "MockOrganizationMembership",
35
+ "MockOrganizationMembershipsResponse",
36
+ "MockPhoneNumber",
37
+ "MockUser",
38
+ "UserNotFoundError",
39
+ ]
@@ -0,0 +1,189 @@
1
+ from contextlib import contextmanager
2
+ from typing import Any, Generator
3
+
4
+ from pytest_clerk_mock.models.auth import MockAuthResult, MockClerkUser
5
+ from pytest_clerk_mock.models.organization import (
6
+ MockOrganization,
7
+ MockOrganizationMembership,
8
+ MockOrganizationMembershipsResponse,
9
+ )
10
+ from pytest_clerk_mock.services.auth import MockAuthState
11
+ from pytest_clerk_mock.services.users import MockUsersClient
12
+
13
+
14
+ class MockClerkClient:
15
+ """Mock implementation of Clerk's SDK client."""
16
+
17
+ def __init__(
18
+ self,
19
+ default_user_id: str | None = "user_test_owner",
20
+ default_org_id: str | None = "org_test_123",
21
+ default_org_role: str = "org:admin",
22
+ ) -> None:
23
+ self._users = MockUsersClient()
24
+ self._auth_state = MockAuthState()
25
+ self._memberships: dict[str, list[MockOrganizationMembership]] = {}
26
+
27
+ if default_user_id is not None:
28
+ self._auth_state.configure(default_user_id, default_org_id, default_org_role)
29
+
30
+ @property
31
+ def users(self) -> MockUsersClient:
32
+ """Access the Users API."""
33
+
34
+ return self._users
35
+
36
+ def reset(self) -> None:
37
+ """Reset all mock services."""
38
+
39
+ self._users.reset()
40
+ self._auth_state.reset()
41
+ self._memberships.clear()
42
+
43
+ def authenticate_request(
44
+ self,
45
+ request: Any,
46
+ options: Any = None,
47
+ ) -> MockAuthResult:
48
+ """Mock implementation of Clerk's authenticate_request.
49
+
50
+ Args:
51
+ request: The FastAPI/Starlette request object (ignored in mock)
52
+ options: AuthenticateRequestOptions (ignored in mock)
53
+
54
+ Returns:
55
+ MockAuthResult with current auth state
56
+ """
57
+
58
+ return self._auth_state.get_result()
59
+
60
+ def configure_auth(
61
+ self,
62
+ user_id: str | None,
63
+ org_id: str | None = None,
64
+ org_role: str = "org:admin",
65
+ ) -> None:
66
+ """Configure the authentication state.
67
+
68
+ Args:
69
+ user_id: The user ID to return in auth results (None for unauthenticated)
70
+ org_id: The organization ID for the authenticated user
71
+ org_role: The role of the user in the organization
72
+ """
73
+
74
+ self._auth_state.configure(user_id, org_id, org_role)
75
+
76
+ def configure_auth_from_user(
77
+ self,
78
+ user: MockClerkUser,
79
+ org_id: str | None = None,
80
+ org_role: str = "org:admin",
81
+ ) -> None:
82
+ """Configure auth state using a predefined MockClerkUser.
83
+
84
+ Args:
85
+ user: The MockClerkUser enum value
86
+ org_id: The organization ID for the authenticated user
87
+ org_role: The role of the user in the organization
88
+ """
89
+
90
+ self._auth_state.configure(user.value, org_id, org_role)
91
+
92
+ @contextmanager
93
+ def as_user(
94
+ self,
95
+ user_id: str | None,
96
+ org_id: str | None = None,
97
+ org_role: str = "org:admin",
98
+ ) -> Generator[None, None, None]:
99
+ """Context manager to temporarily switch to a different user.
100
+
101
+ Args:
102
+ user_id: The user ID to use within the context
103
+ org_id: The organization ID for the user
104
+ org_role: The role of the user in the organization
105
+
106
+ Yields:
107
+ None
108
+
109
+ Example:
110
+ with mock_clerk.as_user("user_123", org_id="org_456"):
111
+ # Requests will be authenticated as user_123
112
+ pass
113
+ """
114
+
115
+ previous = self._auth_state.snapshot()
116
+ self._auth_state.configure(user_id, org_id, org_role)
117
+
118
+ try:
119
+ yield
120
+ finally:
121
+ self._auth_state.restore(previous)
122
+
123
+ @contextmanager
124
+ def as_clerk_user(
125
+ self,
126
+ user: MockClerkUser,
127
+ org_id: str | None = None,
128
+ org_role: str = "org:admin",
129
+ ) -> Generator[None, None, None]:
130
+ """Context manager using predefined MockClerkUser.
131
+
132
+ Args:
133
+ user: The MockClerkUser enum value
134
+ org_id: The organization ID for the user
135
+ org_role: The role of the user in the organization
136
+
137
+ Example:
138
+ with mock_clerk.as_clerk_user(MockClerkUser.TEAM_OWNER, org_id="org_123"):
139
+ # Requests will be authenticated as team owner
140
+ pass
141
+ """
142
+
143
+ with self.as_user(user.value, org_id, org_role):
144
+ yield
145
+
146
+ def add_organization_membership(
147
+ self,
148
+ user_id: str,
149
+ org_id: str,
150
+ role: str = "org:member",
151
+ org_name: str = "",
152
+ ) -> MockOrganizationMembership:
153
+ """Add an organization membership for a user.
154
+
155
+ Args:
156
+ user_id: The user ID to add membership for
157
+ org_id: The organization ID
158
+ role: The role in the organization
159
+ org_name: Optional organization name
160
+
161
+ Returns:
162
+ The created membership
163
+ """
164
+
165
+ membership = MockOrganizationMembership(
166
+ id=f"orgmem_{user_id}_{org_id}",
167
+ role=role,
168
+ organization=MockOrganization(id=org_id, name=org_name),
169
+ )
170
+
171
+ if user_id not in self._memberships:
172
+ self._memberships[user_id] = []
173
+
174
+ self._memberships[user_id].append(membership)
175
+
176
+ return membership
177
+
178
+ async def _get_organization_memberships_async(
179
+ self,
180
+ user_id: str,
181
+ ) -> MockOrganizationMembershipsResponse:
182
+ """Get organization memberships for a user (internal async method)."""
183
+
184
+ memberships = self._memberships.get(user_id, [])
185
+
186
+ return MockOrganizationMembershipsResponse(
187
+ data=memberships,
188
+ total_count=len(memberships),
189
+ )
@@ -0,0 +1,148 @@
1
+ from contextlib import contextmanager
2
+ from typing import Generator
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import httpx
6
+ from clerk_backend_api import SDKError
7
+ from clerk_backend_api.models import ClerkErrors
8
+ from clerk_backend_api.models.clerkerror import ClerkError
9
+ from clerk_backend_api.models.clerkerrors import ClerkErrorsData
10
+ from pydantic import BaseModel
11
+
12
+ EMAIL_EXISTS_ERROR_CODE = "form_identifier_exists"
13
+
14
+
15
+ class MockClerkUserResponse(BaseModel):
16
+ """Simple mock Clerk user returned from create_async."""
17
+
18
+ id: str
19
+
20
+
21
+ class MockClerkUserListResponse:
22
+ """Mock response from Clerk users.list_async."""
23
+
24
+ def __init__(self, data: list[MockClerkUserResponse]):
25
+ self.data = data
26
+
27
+
28
+ @contextmanager
29
+ def mock_clerk_user_creation(
30
+ patch_target: str,
31
+ clerk_user_id: str = "user_clerk_mock_123",
32
+ ) -> Generator[MagicMock, None, None]:
33
+ """Mock Clerk user creation API.
34
+
35
+ Args:
36
+ patch_target: The module path to patch.
37
+ clerk_user_id: The ID to return for the created user.
38
+
39
+ Yields:
40
+ The mock object for assertions.
41
+
42
+ Example:
43
+ with mock_clerk_user_creation("api.core.clerk.clerk.users.create_async", "user_123") as mock:
44
+ # Your test code here
45
+ mock.assert_called_once()
46
+ """
47
+
48
+ with patch(patch_target) as mock_create:
49
+ mock_create.return_value = MockClerkUserResponse(id=clerk_user_id)
50
+ yield mock_create
51
+
52
+
53
+ @contextmanager
54
+ def mock_clerk_user_creation_failure(
55
+ patch_target: str,
56
+ error_message: str = "Clerk API error",
57
+ ) -> Generator[MagicMock, None, None]:
58
+ """Mock Clerk user creation API to simulate a failure.
59
+
60
+ Args:
61
+ patch_target: The module path to patch.
62
+ error_message: The error message to include.
63
+
64
+ Yields:
65
+ The mock object for assertions.
66
+
67
+ Example:
68
+ with mock_clerk_user_creation_failure("api.core.clerk.clerk.users.create_async") as mock:
69
+ with pytest.raises(ErrorResponse):
70
+ # Your test code here
71
+ """
72
+
73
+ mock_response = MagicMock()
74
+ mock_response.status_code = 500
75
+ mock_response.text = error_message
76
+ mock_response.headers = {}
77
+
78
+ with patch(patch_target) as mock_create:
79
+ mock_create.side_effect = SDKError(error_message, mock_response)
80
+ yield mock_create
81
+
82
+
83
+ @contextmanager
84
+ def mock_clerk_user_exists(
85
+ create_patch_target: str,
86
+ list_patch_target: str,
87
+ existing_clerk_user_id: str = "user_clerk_existing_123",
88
+ ) -> Generator[tuple[MagicMock, MagicMock], None, None]:
89
+ """Mock Clerk user creation to simulate an email already exists scenario.
90
+
91
+ This simulates the case where create_async fails because the email is taken,
92
+ but list_async returns the existing user so we can link it.
93
+
94
+ Args:
95
+ create_patch_target: The module path to patch for create_async.
96
+ list_patch_target: The module path to patch for list_async.
97
+ existing_clerk_user_id: The ID of the existing Clerk user to return.
98
+
99
+ Yields:
100
+ A tuple of (mock_create, mock_list) for assertions.
101
+
102
+ Example:
103
+ with mock_clerk_user_exists(
104
+ "api.core.clerk.clerk.users.create_async",
105
+ "api.core.clerk.clerk.users.list_async",
106
+ "user_existing_123"
107
+ ) as (mock_create, mock_list):
108
+ # Your test code here
109
+ mock_create.assert_called_once()
110
+ mock_list.assert_called_once()
111
+ """
112
+
113
+ mock_response = httpx.Response(
114
+ status_code=422,
115
+ json={
116
+ "errors": [
117
+ {
118
+ "message": "That email address is taken. Please try another.",
119
+ "long_message": "That email address is taken. Please try another.",
120
+ "code": EMAIL_EXISTS_ERROR_CODE,
121
+ }
122
+ ]
123
+ },
124
+ )
125
+
126
+ email_exists_error = ClerkErrors(
127
+ data=ClerkErrorsData(
128
+ errors=[
129
+ ClerkError(
130
+ message="That email address is taken. Please try another.",
131
+ long_message="That email address is taken. Please try another.",
132
+ code=EMAIL_EXISTS_ERROR_CODE,
133
+ )
134
+ ]
135
+ ),
136
+ raw_response=mock_response,
137
+ )
138
+
139
+ with (
140
+ patch(create_patch_target) as mock_create,
141
+ patch(list_patch_target) as mock_list,
142
+ ):
143
+ mock_create.side_effect = email_exists_error
144
+ mock_list.return_value = MockClerkUserListResponse(
145
+ data=[MockClerkUserResponse(id=existing_clerk_user_id)]
146
+ )
147
+ yield mock_create, mock_list
148
+
@@ -0,0 +1,18 @@
1
+ from pytest_clerk_mock.models.auth import MockAuthResult, MockClerkUser
2
+ from pytest_clerk_mock.models.organization import (
3
+ MockOrganization,
4
+ MockOrganizationMembership,
5
+ MockOrganizationMembershipsResponse,
6
+ )
7
+ from pytest_clerk_mock.models.user import MockEmailAddress, MockPhoneNumber, MockUser
8
+
9
+ __all__ = [
10
+ "MockAuthResult",
11
+ "MockClerkUser",
12
+ "MockEmailAddress",
13
+ "MockOrganization",
14
+ "MockOrganizationMembership",
15
+ "MockOrganizationMembershipsResponse",
16
+ "MockPhoneNumber",
17
+ "MockUser",
18
+ ]
@@ -0,0 +1,50 @@
1
+ from enum import Enum
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class MockClerkUser(Enum):
8
+ """Predefined user types for common test scenarios."""
9
+
10
+ TEAM_OWNER = "user_test_owner"
11
+ TEAM_MEMBER = "user_test_member"
12
+ GUEST = "user_test_guest"
13
+ UNAUTHENTICATED = None
14
+
15
+
16
+ class MockAuthResult(BaseModel):
17
+ """Mock result from Clerk authenticate_request."""
18
+
19
+ is_signed_in: bool = False
20
+ payload: dict[str, Any] = Field(default_factory=dict)
21
+
22
+ @property
23
+ def is_authenticated(self) -> bool:
24
+ """Alias for is_signed_in."""
25
+
26
+ return self.is_signed_in
27
+
28
+ @classmethod
29
+ def signed_in(
30
+ cls,
31
+ user_id: str,
32
+ org_id: str | None = None,
33
+ org_role: str = "org:admin",
34
+ ) -> "MockAuthResult":
35
+ """Create a signed-in auth result."""
36
+
37
+ return cls(
38
+ is_signed_in=True,
39
+ payload={
40
+ "sub": user_id,
41
+ "org_id": org_id,
42
+ "org_role": org_role,
43
+ },
44
+ )
45
+
46
+ @classmethod
47
+ def signed_out(cls) -> "MockAuthResult":
48
+ """Create a signed-out auth result."""
49
+
50
+ return cls(is_signed_in=False, payload={})
@@ -0,0 +1,30 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class MockOrganization(BaseModel):
5
+ """Represents a Clerk Organization."""
6
+
7
+ id: str
8
+ name: str = ""
9
+ slug: str = ""
10
+ created_at: int = 0
11
+ updated_at: int = 0
12
+
13
+
14
+ class MockOrganizationMembership(BaseModel):
15
+ """Represents a user's membership in an organization."""
16
+
17
+ id: str
18
+ role: str = "org:member"
19
+ organization: MockOrganization | None = None
20
+ public_user_data: dict | None = None
21
+ created_at: int = 0
22
+ updated_at: int = 0
23
+
24
+
25
+ class MockOrganizationMembershipsResponse(BaseModel):
26
+ """Response from get_organization_memberships_async."""
27
+
28
+ data: list[MockOrganizationMembership] = Field(default_factory=list)
29
+ total_count: int = 0
30
+
@@ -0,0 +1,92 @@
1
+ from datetime import datetime
2
+ from typing import Self
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+
7
+ class MockEmailAddress(BaseModel):
8
+ """Represents a Clerk email address object."""
9
+
10
+ model_config = ConfigDict(populate_by_name=True)
11
+
12
+ id: str
13
+ email_address: str
14
+ verification: dict | None = None
15
+ linked_to: list[dict] = Field(default_factory=list)
16
+
17
+ @classmethod
18
+ def create(cls, email: str, email_id: str) -> Self:
19
+ """Create a verified email address."""
20
+
21
+ return cls(
22
+ id=email_id,
23
+ email_address=email,
24
+ verification={"status": "verified", "strategy": "email_code"},
25
+ linked_to=[],
26
+ )
27
+
28
+
29
+ class MockPhoneNumber(BaseModel):
30
+ """Represents a Clerk phone number object."""
31
+
32
+ model_config = ConfigDict(populate_by_name=True)
33
+
34
+ id: str
35
+ phone_number: str
36
+ verification: dict | None = None
37
+ linked_to: list[dict] = Field(default_factory=list)
38
+
39
+ @classmethod
40
+ def create(cls, phone: str, phone_id: str) -> Self:
41
+ """Create a verified phone number."""
42
+
43
+ return cls(
44
+ id=phone_id,
45
+ phone_number=phone,
46
+ verification={"status": "verified", "strategy": "phone_code"},
47
+ linked_to=[],
48
+ )
49
+
50
+
51
+ class MockUser(BaseModel):
52
+ """Represents a Clerk User object."""
53
+
54
+ model_config = ConfigDict(populate_by_name=True)
55
+
56
+ id: str
57
+ external_id: str | None = None
58
+ primary_email_address_id: str | None = None
59
+ primary_phone_number_id: str | None = None
60
+ primary_web3_wallet_id: str | None = None
61
+ username: str | None = None
62
+ first_name: str | None = None
63
+ last_name: str | None = None
64
+ profile_image_url: str = ""
65
+ image_url: str = ""
66
+ has_image: bool = False
67
+ public_metadata: dict = Field(default_factory=dict)
68
+ private_metadata: dict = Field(default_factory=dict)
69
+ unsafe_metadata: dict = Field(default_factory=dict)
70
+ email_addresses: list[MockEmailAddress] = Field(default_factory=list)
71
+ phone_numbers: list[MockPhoneNumber] = Field(default_factory=list)
72
+ web3_wallets: list[dict] = Field(default_factory=list)
73
+ passkeys: list[dict] = Field(default_factory=list)
74
+ password_enabled: bool = False
75
+ two_factor_enabled: bool = False
76
+ totp_enabled: bool = False
77
+ backup_code_enabled: bool = False
78
+ external_accounts: list[dict] = Field(default_factory=list)
79
+ saml_accounts: list[dict] = Field(default_factory=list)
80
+ last_sign_in_at: int | None = None
81
+ banned: bool = False
82
+ locked: bool = False
83
+ lockout_expires_in_seconds: int | None = None
84
+ verification_attempts_remaining: int | None = None
85
+ created_at: int = Field(default_factory=lambda: int(datetime.now().timestamp() * 1000))
86
+ updated_at: int = Field(default_factory=lambda: int(datetime.now().timestamp() * 1000))
87
+ delete_self_enabled: bool = True
88
+ create_organization_enabled: bool = True
89
+ last_active_at: int | None = None
90
+ create_organizations_limit: int | None = None
91
+ legal_accepted_at: int | None = None
92
+