pytest-clerk 1.0.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Munipal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.1
2
+ Name: pytest-clerk
3
+ Version: 1.0.0
4
+ Summary: A set of pytest fixtures to help with integration testing with Clerk.
5
+ Home-page: https://gitlab.com/munipal-oss/pytest-clerk
6
+ License: MIT
7
+ Author: Ryan Causey
8
+ Author-email: ryan.causey@munipal.io
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: Framework :: Pytest
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Provides-Extra: aws
17
+ Requires-Dist: httpx (>=0.27.0,<0.28.0)
18
+ Requires-Dist: pytest (>=8.0.0,<9.0.0)
19
+ Requires-Dist: pytest-aws-fixtures (>=1.2.0,<2.0.0) ; extra == "aws"
20
+ Requires-Dist: python-decouple (>=3.0,<4.0)
21
+ Requires-Dist: tenacity (>=8.2.3,<9.0.0)
22
+ Project-URL: Repository, https://gitlab.com/munipal-oss/pytest-clerk
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Pytest Clerk
26
+
27
+ This is a collection of fixtures we've been using to perform integration tests using
28
+ the real Clerk APIs.
29
+
30
+ Usage of any of these fixture requires that the following be specified in environment
31
+ variables or a .env file:
32
+
33
+ * CLERK_SECRET_KEY: Set this to the value of your Clerk secret key. Conflicts with
34
+ CLERK_SECRET_ID.
35
+ * CLERK_SECRET_ID: Set this to the ID of the AWS SecretsManager Secret that contains the
36
+ Clerk secret key. This requires installing the `aws` extra. Conflicts
37
+ with CLERK_SECRET_KEY.
38
+
39
+ The fixtures themselves are heavily documented so please view their docstrings for more
40
+ information on their usage.
41
+
@@ -0,0 +1,16 @@
1
+ # Pytest Clerk
2
+
3
+ This is a collection of fixtures we've been using to perform integration tests using
4
+ the real Clerk APIs.
5
+
6
+ Usage of any of these fixture requires that the following be specified in environment
7
+ variables or a .env file:
8
+
9
+ * CLERK_SECRET_KEY: Set this to the value of your Clerk secret key. Conflicts with
10
+ CLERK_SECRET_ID.
11
+ * CLERK_SECRET_ID: Set this to the ID of the AWS SecretsManager Secret that contains the
12
+ Clerk secret key. This requires installing the `aws` extra. Conflicts
13
+ with CLERK_SECRET_KEY.
14
+
15
+ The fixtures themselves are heavily documented so please view their docstrings for more
16
+ information on their usage.
@@ -0,0 +1,35 @@
1
+ [tool.poetry]
2
+ name = "pytest-clerk"
3
+ version = "1.0.0"
4
+ description = "A set of pytest fixtures to help with integration testing with Clerk."
5
+ authors = ["Ryan Causey <ryan.causey@munipal.io>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ repository = "https://gitlab.com/munipal-oss/pytest-clerk"
9
+ homepage = "https://gitlab.com/munipal-oss/pytest-clerk"
10
+ packages = [{include = "pytest_clerk"}]
11
+ classifiers = ["Framework :: Pytest"]
12
+
13
+ [tool.poetry.dependencies]
14
+ python = "^3.10"
15
+ pytest = "^8.0.0"
16
+ python-decouple = "^3.0"
17
+ tenacity = "^8.2.3"
18
+ httpx = "^0.27.0"
19
+ pytest-aws-fixtures = {version = "^1.2.0", optional = true}
20
+
21
+ [tool.poetry.group.dev.dependencies]
22
+ prospector = "^1.10.2"
23
+ black = "^23.3.0"
24
+ isort = "^5.12.0"
25
+ pre-commit = "^3.7.0"
26
+
27
+ [tool.poetry.extras]
28
+ aws = ["pytest-aws-fixtures"]
29
+
30
+ [tool.poetry.plugins."pytest11"]
31
+ pytest_clerk = "pytest_clerk.clerk"
32
+
33
+ [build-system]
34
+ requires = ["poetry-core"]
35
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,445 @@
1
+ from contextlib import suppress
2
+
3
+ import httpx
4
+ import pytest
5
+ from decouple import UndefinedValueError, config
6
+ from tenacity import retry, retry_if_exception, wait_random_exponential
7
+
8
+ retryable_status_codes = (
9
+ httpx.codes.TOO_MANY_REQUESTS,
10
+ httpx.codes.INTERNAL_SERVER_ERROR,
11
+ httpx.codes.BAD_GATEWAY,
12
+ httpx.codes.SERVICE_UNAVAILABLE,
13
+ httpx.codes.GATEWAY_TIMEOUT,
14
+ )
15
+
16
+
17
+ def retry_predicate(exception):
18
+ """Return whether to retry. This depends on getting an exception with a response
19
+ object, and that response object having one of the status codes that we can retry.
20
+ """
21
+ return (
22
+ hasattr(exception, "response")
23
+ and exception.response.status_code in retryable_status_codes
24
+ )
25
+
26
+
27
+ @pytest.fixture(scope="session")
28
+ def clerk_secret_key(request):
29
+ """Retrieve the clerk secret key to use for the test.
30
+
31
+ If using AWS Secrets Manager, the CLERK_SECRET_ID variable be set to the ID of the
32
+ SecretsManager secret that contains the Clerk secret key. This can be set in a .env
33
+ file or an environment variable.
34
+
35
+ If not using AWS Secrets Manager, the CLERK_SECRET_KEY variable must be set to the
36
+ value of the clerk secret key to use. This can be set in a .env file or an
37
+ environment variable.
38
+ """
39
+ with suppress(UndefinedValueError, pytest.FixtureLookupError):
40
+ secretsmanager_client = request.getfixturevalue("secretsmanager_client")
41
+ return secretsmanager_client.get_secret_value(
42
+ SecretId=config("CLERK_SECRET_ID")
43
+ )["SecretString"]
44
+
45
+ with suppress(UndefinedValueError):
46
+ return config("CLERK_SECRET_KEY")
47
+
48
+ pytest.skip(
49
+ reason="Neither CLERK_SECRET_ID nor CLERK_SECRET_KEY was found in the"
50
+ " environment or a .env file and is required for this test. If CLERK_SECRET_ID"
51
+ " is set, and you're still seeing this message, ensure the aws extra"
52
+ " dependencies are installed."
53
+ )
54
+
55
+
56
+ @pytest.fixture(scope="session")
57
+ def clerk_backend_httpx_client(clerk_secret_key):
58
+ """A fixture that creates a HTTPX Client instance with the required backend Clerk
59
+ Authorization headers set and the correct Clerk backend API base URL.
60
+
61
+ Please be mindful of the Clerk API rate limits:
62
+ https://clerk.com/docs/reference/rate-limits
63
+ """
64
+ client = httpx.Client(
65
+ headers={"Authorization": f"Bearer {clerk_secret_key}"},
66
+ base_url="https://api.clerk.com/v1",
67
+ )
68
+
69
+ yield client
70
+
71
+ client.close()
72
+
73
+
74
+ @pytest.fixture(scope="session")
75
+ def clerk_frontend_api_url():
76
+ """This fixture returns the value of the CLERK_FRONTEND_URL variable and is used to
77
+ make calls to the Clerk frontend API.
78
+
79
+ CLERK_FRONTEND_URL can be set via environment variables or in a .env file. This URL
80
+ can be found under Developers -> API Keys -> Show API URLs.
81
+ """
82
+ with suppress(UndefinedValueError):
83
+ return config("CLERK_FRONTEND_URL")
84
+
85
+ pytest.skip(
86
+ reason="CLERK_FRONTEND_URL was not found in the environment or a .env file and"
87
+ " is required for this test."
88
+ )
89
+
90
+
91
+ @pytest.fixture(scope="session")
92
+ def clerk_frontend_httpx_client(clerk_frontend_api_url):
93
+ """This fixture returns a function that creates an HTTPX Client instance with the
94
+ required frontend Clerk Authorization parameters set and the correct Clerk frontend
95
+ API base URL.
96
+
97
+ This requires the CLERK_FRONTEND_URL variable to be set. CLERK_FRONTEND_URL can be
98
+ set via environment variables or in a .env file. This URL can be found under
99
+ Developers -> API Keys -> Show API URLs.
100
+ """
101
+ with httpx.Client(base_url=f"{clerk_frontend_api_url}/v1") as client:
102
+ result = client.post(url="/dev_browser")
103
+
104
+ result.raise_for_status()
105
+
106
+ client = httpx.Client(
107
+ params={"__dev_session": result.json()["token"]},
108
+ base_url=clerk_frontend_api_url,
109
+ )
110
+
111
+ yield client
112
+
113
+ client.close()
114
+
115
+
116
+ @pytest.fixture
117
+ def clerk_delete_org(clerk_backend_httpx_client):
118
+ """This fixture provides a function to delete an organization given an org ID. Any
119
+ additional kwargs are passed through to the httpx.Client.delete call.
120
+
121
+ The API documentation for this call can be found below:
122
+ https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/DeleteOrganization
123
+ """
124
+
125
+ @retry(
126
+ retry=retry_if_exception(predicate=retry_predicate),
127
+ wait=wait_random_exponential(multiplier=0.5, max=60),
128
+ )
129
+ def _inner(org_id, **kwargs):
130
+ """Delete the org with the given org ID. Any additional kwargs are passed
131
+ through to the httpx.Client.delete call.
132
+
133
+ This will retry rate limit errors.
134
+
135
+ The API documentation for this call can be found below:
136
+ https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/DeleteOrganization
137
+ """
138
+ return clerk_backend_httpx_client.delete(
139
+ url=f"/organizations/{org_id}", **kwargs
140
+ )
141
+
142
+ return _inner
143
+
144
+
145
+ @pytest.fixture
146
+ def clerk_create_org(clerk_backend_httpx_client, clerk_delete_org):
147
+ """This fixture provides a function to create an organization that will
148
+ automatically be deleted on fixture teardown.
149
+
150
+ The API documentation for this call can be found below:
151
+ https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/CreateOrganization
152
+ """
153
+ orgs_to_delete = []
154
+
155
+ @retry(
156
+ retry=retry_if_exception(predicate=retry_predicate),
157
+ wait=wait_random_exponential(multiplier=0.5, max=60),
158
+ )
159
+ def _inner(organization_data, **kwargs):
160
+ """This function creates an Organization with the provided organization_data,
161
+ and saves the reference to delete it at a later time. All additional kwargs are
162
+ passed through to the httpx.Client.post call.
163
+
164
+ This will retry rate limit errors.
165
+
166
+ The API documentation for this call can be found below:
167
+ https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/CreateOrganization
168
+ """
169
+ nonlocal orgs_to_delete
170
+
171
+ result = clerk_backend_httpx_client.post(
172
+ url="/organizations", json=organization_data, **kwargs
173
+ )
174
+ result.raise_for_status()
175
+ result = result.json()
176
+ orgs_to_delete.append(result)
177
+ return result
178
+
179
+ yield _inner
180
+
181
+ # Now remove all of the orgs.
182
+ for org in orgs_to_delete:
183
+ clerk_delete_org(org_id=org["id"])
184
+
185
+
186
+ @pytest.fixture
187
+ def clerk_get_org(clerk_backend_httpx_client):
188
+ """This fixture provides a function to get an organization by its ID or slug. All
189
+ additional kwargs are passed through to the httpx.Client.get call.
190
+
191
+ The API documentation for this call can be found below:
192
+ https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/GetOrganization
193
+ """
194
+
195
+ @retry(
196
+ retry=retry_if_exception(predicate=retry_predicate),
197
+ wait=wait_random_exponential(multiplier=0.5, max=60),
198
+ )
199
+ def _inner(org_id_or_slug, **kwargs):
200
+ """This function attempts to find and return the org with the given ID or slug.
201
+ All additional kwargs are passed through to the httpx.Client.get call.
202
+
203
+ This will retry rate limit errors.
204
+
205
+ The API documentation for this call can be found below:
206
+ https://clerk.com/docs/reference/backend-api/tag/Organizations#operation/GetOrganization
207
+ """
208
+ result = clerk_backend_httpx_client.get(
209
+ url=f"/organizations/{org_id_or_slug}", **kwargs
210
+ )
211
+ result.raise_for_status()
212
+ return result.json()
213
+
214
+ yield _inner
215
+
216
+
217
+ @pytest.fixture
218
+ def clerk_delete_user(clerk_backend_httpx_client):
219
+ """This fixture provides a function to delete a user given the user ID. All
220
+ additional kwargs are passed through to the httpx.Client.delete call.
221
+
222
+ The API documentation for this call can be found below:
223
+ https://clerk.com/docs/reference/backend-api/tag/Users#operation/DeleteUser
224
+ """
225
+
226
+ @retry(
227
+ retry=retry_if_exception(predicate=retry_predicate),
228
+ wait=wait_random_exponential(multiplier=0.5, max=60),
229
+ )
230
+ def _inner(user_id, **kwargs):
231
+ """Delete the user with the given user ID. All additional kwargs are passed
232
+ through to the httpx.Client.delete call.
233
+
234
+ This will retry rate limit errors.
235
+
236
+ The API documentation for this call can be found below:
237
+ https://clerk.com/docs/reference/backend-api/tag/Users#operation/DeleteUser
238
+ """
239
+ return clerk_backend_httpx_client.delete(url=f"/users/{user_id}", **kwargs)
240
+
241
+ return _inner
242
+
243
+
244
+ @pytest.fixture
245
+ def clerk_create_user(clerk_backend_httpx_client, clerk_delete_user):
246
+ """This fixture provides a method to create a user that will automatically
247
+ be deleted on fixture teardown.
248
+
249
+ The API documentation for this call can be found below:
250
+ https://clerk.com/docs/reference/backend-api/tag/Users#operation/CreateUser
251
+ """
252
+ users_to_delete = []
253
+
254
+ @retry(
255
+ retry=retry_if_exception(predicate=retry_predicate),
256
+ wait=wait_random_exponential(multiplier=0.5, max=60),
257
+ )
258
+ def _inner(user_data, **kwargs):
259
+ """This function uses user_data to create a User with the backend API, and
260
+ saves the reference to delete it at a later time. All other kwargs are passed
261
+ through to the httpx.Client.post call.
262
+
263
+ This will retry rate limit errors.
264
+
265
+ The API documentation for this call can be found below:
266
+ https://clerk.com/docs/reference/backend-api/tag/Users#operation/CreateUser
267
+ """
268
+ nonlocal users_to_delete
269
+ result = clerk_backend_httpx_client.post(url="/users", json=user_data, **kwargs)
270
+ result.raise_for_status()
271
+ result = result.json()
272
+ users_to_delete.append(result)
273
+ return result
274
+
275
+ yield _inner
276
+
277
+ # Now remove all of the users.
278
+ for user in users_to_delete:
279
+ clerk_delete_user(user_id=user["id"])
280
+
281
+
282
+ @pytest.fixture
283
+ def clerk_add_org_member(clerk_backend_httpx_client):
284
+ """This fixture provides a function to add a user to an organization. All additional
285
+ kwargs are passed through to the httpx.Client.post call.
286
+
287
+ The API documentation for this call can be found below:
288
+ https://clerk.com/docs/reference/backend-api/tag/Organization-Memberships#operation/CreateOrganizationMembership
289
+ """
290
+
291
+ @retry(
292
+ retry=retry_if_exception(predicate=retry_predicate),
293
+ wait=wait_random_exponential(multiplier=0.5, max=60),
294
+ )
295
+ def _inner(org_id, user_id, role, **kwargs):
296
+ """Add's the provided user ID to the provided org ID with the provided role. All
297
+ additional kwargs are passed through to the httpx.Client.post call.
298
+
299
+ This will retry rate limit errors.
300
+
301
+ The API documentation for this call can be found below:
302
+ https://clerk.com/docs/reference/backend-api/tag/Organization-Memberships#operation/CreateOrganizationMembership
303
+ """
304
+ result = clerk_backend_httpx_client.post(
305
+ url=f"/organizations/{org_id}/memberships",
306
+ json={"user_id": user_id, "role": role},
307
+ **kwargs,
308
+ )
309
+ result.raise_for_status()
310
+ return result.json()
311
+
312
+ return _inner
313
+
314
+
315
+ @pytest.fixture
316
+ def clerk_sign_user_in(clerk_frontend_httpx_client):
317
+ """This fixture returns a function that, given a Clerk User's email and password,
318
+ will sign that user in and return the resulting sign in object from the front end
319
+ API. All additional kwargs are passed through to the httpx.Client.post call.
320
+
321
+ The API documentation for this call can be found below:
322
+ https://clerk.com/docs/reference/frontend-api/tag/Sign-Ins#operation/createSignIn
323
+ """
324
+
325
+ @retry(
326
+ retry=retry_if_exception(predicate=retry_predicate),
327
+ wait=wait_random_exponential(multiplier=0.5, max=60),
328
+ )
329
+ def _inner(email, password, **kwargs):
330
+ """Attempts to sign in the user using the provided email and password, and then
331
+ returns the sign in object. All additional kwargs are passed through to the
332
+ httpx.Client.post call.
333
+
334
+ This will retry rate limit errors.
335
+
336
+ The API documentation for this call can be found below:
337
+ https://clerk.com/docs/reference/frontend-api/tag/Sign-Ins#operation/createSignIn
338
+ """
339
+ result = clerk_frontend_httpx_client.post(
340
+ url="/client/sign_ins",
341
+ data={"strategy": "password", "identifier": email, "password": password},
342
+ **kwargs,
343
+ )
344
+ result.raise_for_status()
345
+ result = result.json()
346
+ assert result["response"]["status"] == "complete"
347
+ return result
348
+
349
+ return _inner
350
+
351
+
352
+ @pytest.fixture
353
+ def clerk_touch_user_session(clerk_frontend_httpx_client):
354
+ """This fixture returns a function that, given a Clerk user session ID and any
355
+ optional session_data, touch the session with the given ID with any session_data
356
+ sent as form data. This passes through any additional kwargs to the
357
+ httpx.Client.post call.
358
+
359
+ The API documentation for this call can be found below:
360
+ https://clerk.com/docs/reference/frontend-api/tag/Sessions#operation/touchSession
361
+ """
362
+
363
+ @retry(
364
+ retry=retry_if_exception(predicate=retry_predicate),
365
+ wait=wait_random_exponential(multiplier=0.5, max=60),
366
+ )
367
+ def _inner(session_id, session_data, **kwargs):
368
+ """Given a Clerk user session ID and any optional session_data, touch the
369
+ session with the given ID with any session_data sent as form data. This passes
370
+ through any additional kwargs to the httpx.Client.post call.
371
+
372
+ This will retry rate limit errors.
373
+
374
+ The API documentation for this call can be found below:
375
+ https://clerk.com/docs/reference/frontend-api/tag/Sessions#operation/touchSession
376
+ """
377
+ result = clerk_frontend_httpx_client.post(
378
+ url=f"/client/sessions/{session_id}/touch", data=session_data, **kwargs
379
+ )
380
+ result.raise_for_status()
381
+ return result.json()
382
+
383
+ return _inner
384
+
385
+
386
+ @pytest.fixture
387
+ def clerk_set_user_active_org(clerk_touch_user_session):
388
+ """This fixture returns a function that, given a Clerk user session ID and an
389
+ organization ID, attempts to set that organization as active.
390
+
391
+ The user must already be a member of the organization for this to work.
392
+
393
+ Any additional kwargs are passed through to the httpx.Client.post call.
394
+ """
395
+
396
+ def _inner(session_id, org_id, **kwargs):
397
+ """Given a Clerk user session ID and an organization ID, this function attempts
398
+ to set that organization as active.
399
+
400
+ The user must already be a member of the organization for this to work.
401
+
402
+ Any additional kwargs are passed through to the httpx.Client.post call.
403
+ """
404
+ return clerk_touch_user_session(
405
+ session_id=session_id,
406
+ session_data={"active_organization_id": org_id},
407
+ **kwargs,
408
+ )
409
+
410
+ return _inner
411
+
412
+
413
+ @pytest.fixture
414
+ def clerk_get_user_session_token(clerk_frontend_httpx_client):
415
+ """This fixture returns a function that, given a Clerk session ID, will retrieve a
416
+ currently valid session token for the user tied to that session.
417
+
418
+ Any additional kwargs are passed through to the httpx.Client.post call.
419
+
420
+ The API documentation for this call can be found below:
421
+ https://clerk.com/docs/reference/frontend-api/tag/Sessions#operation/createSessionToken
422
+ """
423
+
424
+ @retry(
425
+ retry=retry_if_exception(predicate=retry_predicate),
426
+ wait=wait_random_exponential(multiplier=0.5, max=60),
427
+ )
428
+ def _inner(session_id, **kwargs):
429
+ """Retrieves a currently valid session token for the user tied to the provided
430
+ session ID.
431
+
432
+ Any additional kwargs are passed through to the httpx.Client.post call.
433
+
434
+ This will retry rate limit errors.
435
+
436
+ The API documentation for this call can be found below:
437
+ https://clerk.com/docs/reference/frontend-api/tag/Sessions#operation/createSessionToken
438
+ """
439
+ result = clerk_frontend_httpx_client.post(
440
+ url=f"/client/sessions/{session_id}/tokens", **kwargs
441
+ )
442
+ result.raise_for_status()
443
+ return result.json()["jwt"]
444
+
445
+ return _inner