Skip to content

Commit 265f913

Browse files
refactor: format code and add test infrastructure (#19)
* refactor: format code and add test infrastructure * chore: remove flake8-complexity (COM) rule from ruff linting config * fix: update ruff formatting check command to use correct syntax * fix: update ruff formatting command syntax to use check subcommand * fix: remove unused variables in test files for routes and stateless store * fix: remove unused variables in test files for routes and stateless store * ci: rename test workflow to Unit Tests * test: verify redirect URI construction in AuthClient initialization * style: remove trailing whitespace in test_auth_client.py
1 parent 26ef4e9 commit 265f913

20 files changed

+2305
-422
lines changed

.github/workflows/tests.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Unit Tests
2+
3+
on:
4+
push:
5+
branches: [ main, master]
6+
pull_request:
7+
branches: [ main, master]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ['3.9', '3.10', '3.11', '3.12']
15+
16+
steps:
17+
- uses: actions/checkout@v3
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v4
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install Poetry
25+
run: |
26+
curl -sSL https://install.python-poetry.org | python3 -
27+
poetry --version
28+
29+
- name: Install dependencies
30+
run: |
31+
poetry install
32+
33+
- name: Check formatting
34+
run: |
35+
poetry run ruff check .
36+
37+
- name: Run tests with coverage
38+
run: |
39+
poetry run pytest --cov=auth0_fastapi --cov-report=xml --cov-report=term-missing

.ruff.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
line-length = 120
2+
target-version = "py39"
3+
select = [
4+
"E", # pycodestyle errors
5+
"W", # pycodestyle warnings
6+
"F", # pyflakes
7+
"I", # isortbear
8+
"C4", # flake8-comprehensions
9+
"UP", # pyupgrade
10+
"S", # bandit (security)
11+
"DTZ", # flake8-datetimez
12+
"G", # flake8-logging-format
13+
"A", # flake8-annotations
14+
"C", # flake8-coding¯
15+
]
16+
ignore = ["B904"]
17+
18+
[per-file-ignores]
19+
"__init__.py" = ["F401", "F811"]
20+
"src/auth0_fastapi/config.py" = ["E501"]
21+
"src/auth0_fastapi/server/routes.py" = ["C901"]
22+
"src/auth0_fastapi/test/**/*.py" = ["F841", "S101", "COM812","S105", "S106"]

poetry.lock

Lines changed: 201 additions & 152 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ packages = [
1313
[tool.poetry.dependencies]
1414
python = ">=3.9"
1515
auth0-server-python = ">=1.0.0b3"
16-
fastapi = "^0.115.11"
16+
fastapi = ">=0.115.11,<0.117.0"
1717
itsdangerous = "^2.2.0"
1818

1919

@@ -24,9 +24,10 @@ pytest-asyncio = "^0.20.3"
2424
pytest-mock = "^3.14.0"
2525
uvicorn = "^0.34.0"
2626
twine = "^6.1.0"
27+
ruff = "^0.12.7"
2728

2829
[tool.pytest.ini_options]
29-
addopts = "--cov=auth_server --cov-report=term-missing:skip-covered --cov-report=xml"
30+
addopts = "--cov=auth0_fastapi --cov-report=term-missing --cov-report=html"
3031

3132
[build-system]
3233
requires = ["poetry-core>=1.4.0"]

requirements.txt

Lines changed: 12 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,17 @@
1-
annotated-types==0.7.0
2-
anyio==4.9.0
1+
# Direct dependencies
32
auth0-server-python==1.0.0b4
4-
Authlib==1.5.2
5-
backports.tarfile==1.2.0
6-
certifi==2025.1.31
7-
cffi==1.17.1
8-
charset-normalizer==3.4.1
9-
click==8.1.8
10-
coverage==7.8.0
11-
cryptography==44.0.1
12-
docutils==0.21.2
13-
exceptiongroup==1.3.0
14-
fastapi==0.115.12
15-
h11==0.16.0
16-
httpcore==1.0.9
17-
httpx==0.28.1
18-
id==1.5.0
19-
idna==3.10
20-
importlib_metadata==8.6.1
21-
iniconfig==2.1.0
3+
Authlib==1.6.1
4+
fastapi==0.116.1
225
itsdangerous==2.2.0
23-
jaraco.classes==3.4.0
24-
jaraco.context==6.0.1
25-
jaraco.functools==4.1.0
26-
jwcrypto==1.5.6
27-
keyring==25.6.0
28-
markdown-it-py==3.0.0
29-
mdurl==0.1.2
30-
more-itertools==10.6.0
31-
nh3==0.2.21
32-
packaging==24.2
33-
pluggy==1.6.0
34-
pycparser==2.22
35-
pydantic==2.11.2
36-
pydantic_core==2.33.1
37-
Pygments==2.19.1
38-
PyJWT==2.10.1
6+
pydantic==2.11.7
7+
pydantic_core==2.33.2
8+
starlette==0.47.2
9+
typing_extensions==4.14.1
10+
uvicorn==0.34.3
3911
pytest==7.4.4
40-
pytest-asyncio==0.23.8
12+
pytest-asyncio==0.20.3
4113
pytest-cov==4.1.0
42-
pytest-mock==3.14.0
43-
readme_renderer==44.0
44-
requests==2.32.4
45-
requests-toolbelt==1.0.0
46-
rfc3986==2.0.0
47-
rich==14.0.0
48-
sniffio==1.3.1
49-
starlette==0.46.1
50-
tomli==2.2.1
14+
pytest-mock==3.14.1
15+
ruff==0.12.7
5116
twine==6.1.0
52-
typing-inspection==0.4.1
53-
typing_extensions==4.13.1
54-
urllib3==2.4.0
55-
uvicorn==0.34.0
56-
zipp==3.21.0
17+
coverage==7.10.2

src/auth0_fastapi/auth/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
from .auth_client import AuthClient
22

3-
43
__all__ = ["AuthClient"]

src/auth0_fastapi/auth/auth_client.py

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11

2-
from fastapi import Request, Response, HTTPException, status
2+
# Imported from auth0-server-python
3+
from auth0_server_python.auth_server.server_client import ServerClient
4+
from auth0_server_python.auth_types import LogoutOptions, StartInteractiveLoginOptions
5+
from fastapi import HTTPException, Request, Response, status
36

7+
from auth0_fastapi.config import Auth0Config
48
from auth0_fastapi.stores.cookie_transaction_store import CookieTransactionStore
59
from auth0_fastapi.stores.stateless_state_store import StatelessStateStore
610

7-
from auth0_fastapi.config import Auth0Config
8-
9-
# Imported from auth0-server-python
10-
from auth0_server_python.auth_server.server_client import ServerClient
11-
from auth0_server_python.auth_types import (
12-
StartInteractiveLoginOptions,
13-
LogoutOptions
14-
)
15-
1611

1712
class AuthClient:
1813
"""
@@ -22,7 +17,12 @@ class AuthClient:
2217
logging out, and handling backchannel logout.
2318
"""
2419

25-
def __init__(self, config: Auth0Config, state_store=None, transaction_store=None):
20+
def __init__(
21+
self,
22+
config: Auth0Config,
23+
state_store=None,
24+
transaction_store=None,
25+
):
2626
self.config = config
2727
# Build the redirect URI based on the provided app_base_url
2828
redirect_uri = f"{str(config.app_base_url).rstrip('/')}/auth/callback"
@@ -48,11 +48,16 @@ def __init__(self, config: Auth0Config, state_store=None, transaction_store=None
4848
authorization_params={
4949
"audience": config.audience,
5050
"redirect_uri": redirect_uri,
51-
**(config.authorization_params or {})
51+
**(config.authorization_params or {}),
5252
},
5353
)
5454

55-
async def start_login(self, app_state: dict = None, authorization_params: dict = None, store_options: dict = None) -> str:
55+
async def start_login(
56+
self,
57+
app_state: dict = None,
58+
authorization_params: dict = None,
59+
store_options: dict = None,
60+
) -> str:
5661
"""
5762
Initiates the interactive login process.
5863
Optionally, an app_state dictionary can be passed to persist additional state.
@@ -62,32 +67,47 @@ async def start_login(self, app_state: dict = None, authorization_params: dict =
6267
options = StartInteractiveLoginOptions(
6368
pushed_authorization_requests=pushed_authorization_requests,
6469
app_state=app_state,
65-
authorization_params=authorization_params if not pushed_authorization_requests else None
70+
authorization_params=authorization_params if not pushed_authorization_requests else None,
6671
)
6772
return await self.client.start_interactive_login(options, store_options=store_options)
6873

69-
async def complete_login(self, callback_url: str, store_options: dict = None) -> dict:
74+
async def complete_login(
75+
self,
76+
callback_url: str,
77+
store_options: dict = None,
78+
) -> dict:
7079
"""
7180
Completes the interactive login process using the callback URL.
7281
Returns a dictionary with the session state data.
7382
"""
7483
return await self.client.complete_interactive_login(callback_url, store_options=store_options)
7584

76-
async def logout(self, return_to: str = None, store_options: dict = None) -> str:
85+
async def logout(
86+
self,
87+
return_to: str = None,
88+
store_options: dict = None,
89+
) -> str:
7790
"""
7891
Initiates logout by clearing the session and generating a logout URL.
7992
Optionally accepts a return_to URL for redirection after logout.
8093
"""
8194
options = LogoutOptions(return_to=return_to)
8295
return await self.client.logout(options, store_options=store_options)
8396

84-
async def handle_backchannel_logout(self, logout_token: str) -> None:
97+
async def handle_backchannel_logout(
98+
self,
99+
logout_token: str,
100+
) -> None:
85101
"""
86102
Processes a backchannel logout using the provided logout token.
87103
"""
88104
return await self.client.handle_backchannel_logout(logout_token)
89105

90-
async def start_link_user(self, options: dict, store_options: dict = None) -> str:
106+
async def start_link_user(
107+
self,
108+
options: dict,
109+
store_options: dict = None,
110+
) -> str:
91111
"""
92112
Initiates the user linking process.
93113
Options should include:
@@ -99,15 +119,23 @@ async def start_link_user(self, options: dict, store_options: dict = None) -> st
99119
"""
100120
return await self.client.start_link_user(options, store_options=store_options)
101121

102-
async def complete_link_user(self, url: str, store_options: dict = None) -> dict:
122+
async def complete_link_user(
123+
self,
124+
url: str,
125+
store_options: dict = None,
126+
) -> dict:
103127
"""
104128
Completes the user linking process.
105129
The provided URL should be the callback URL from Auth0.
106130
Returns a dictionary containing the original appState.
107131
"""
108132
return await self.client.complete_link_user(url, store_options=store_options)
109133

110-
async def start_unlink_user(self, options: dict, store_options: dict = None) -> str:
134+
async def start_unlink_user(
135+
self,
136+
options: dict,
137+
store_options: dict = None,
138+
) -> str:
111139
"""
112140
Initiates the user unlinking process.
113141
Options should include:
@@ -118,15 +146,23 @@ async def start_unlink_user(self, options: dict, store_options: dict = None) ->
118146
"""
119147
return await self.client.start_unlink_user(options, store_options=store_options)
120148

121-
async def complete_unlink_user(self, url: str, store_options: dict = None) -> dict:
149+
async def complete_unlink_user(
150+
self,
151+
url: str,
152+
store_options: dict = None,
153+
) -> dict:
122154
"""
123155
Completes the user unlinking process.
124156
The provided URL should be the callback URL from Auth0.
125157
Returns a dictionary containing the original appState.
126158
"""
127159
return await self.client.complete_unlink_user(url, store_options=store_options)
128160

129-
async def require_session(self, request: Request, response: Response) -> dict:
161+
async def require_session(
162+
self,
163+
request: Request,
164+
response: Response,
165+
) -> dict:
130166
"""
131167
Dependency method to ensure a session exists.
132168
Retrieves the session from the state store using the underlying client.

src/auth0_fastapi/config.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from pydantic import BaseModel, AnyUrl, Field
2-
from typing import Optional, Dict, Any
1+
from typing import Any, Optional
2+
3+
from pydantic import AnyUrl, BaseModel, Field
4+
35

46
class Auth0Config(BaseModel):
57
"""
@@ -11,7 +13,7 @@ class Auth0Config(BaseModel):
1113
app_base_url: AnyUrl = Field(..., alias="appBaseUrl", description="Base URL of your application (e.g., https://example.com)")
1214
secret: str = Field(..., description="Secret used for encryption and signing cookies")
1315
audience: Optional[str] = Field(None, description="Target audience for tokens (if applicable)")
14-
authorization_params: Optional[Dict[str, Any]] = Field(None, description="Additional parameters to include in the authorization request")
16+
authorization_params: Optional[dict[str, Any]] = Field(None, description="Additional parameters to include in the authorization request")
1517
pushed_authorization_requests: bool = Field(False, description="Whether to use pushed authorization requests")
1618
# Route-mounting flags with desired defaults
1719
mount_routes: bool = Field(True, description="Controls /auth/* routes: login, logout, callback, backchannel-logout")

src/auth0_fastapi/errors/__init__.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
from fastapi import Request, HTTPException
2-
from fastapi.responses import JSONResponse
3-
41
#Imported from auth0-server-python
52
from auth0_server_python.error import (
6-
Auth0Error,
7-
MissingTransactionError,
8-
ApiError,
93
AccessTokenError,
10-
MissingRequiredArgumentError,
4+
AccessTokenForConnectionError,
5+
ApiError,
6+
Auth0Error,
117
BackchannelLogoutError,
12-
AccessTokenForConnectionError
8+
MissingRequiredArgumentError,
9+
MissingTransactionError,
1310
)
11+
from fastapi import Request
12+
from fastapi.responses import JSONResponse
13+
1414

1515
def auth0_exception_handler(request: Request, exc: Auth0Error):
1616
"""
@@ -37,8 +37,8 @@ def auth0_exception_handler(request: Request, exc: Auth0Error):
3737
status_code=status_code,
3838
content={
3939
"error": getattr(exc, "code", "auth_error"),
40-
"message": exc.message or "An authentication error occurred."
41-
}
40+
"message": exc.message or "An authentication error occurred.",
41+
},
4242
)
4343

4444
def register_exception_handlers(app):

0 commit comments

Comments
 (0)