Skip to content

Commit

Permalink
WIP: add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jmaupetit committed May 28, 2024
1 parent 16b9ec1 commit 9afaf3a
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 51 deletions.
5 changes: 4 additions & 1 deletion bin/pytest
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ DOCKER_USER="${DOCKER_UID}:${DOCKER_GID}"
DOCKER_USER=${DOCKER_USER} \
DOCKER_UID=${DOCKER_UID} \
DOCKER_GID=${DOCKER_GID} \
docker compose run --rm api pipenv run pytest "$@"
docker compose run --rm \
-e QUALICHARGE_OIDC_IS_ENABLED=True \
api \
pipenv run pytest "$@"
1 change: 1 addition & 0 deletions env.d/api
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ QUALICHARGE_DB_PORT=5432
QUALICHARGE_DB_USER=qualicharge
QUALICHARGE_DEBUG=1
QUALICHARGE_OIDC_PROVIDER_BASE_URL=http://keycloak:8080/realms/qualicharge
QUALICHARGE_OIDC_IS_ENABLED=False
QUALICHARGE_OAUTH2_TOKEN_ENCODING_KEY=thisissupersecret
QUALICHARGE_OAUTH2_TOKEN_ISSUER=http://localhost:8010
QUALICHARGE_TEST_DB_NAME=test-qualicharge-api
6 changes: 4 additions & 2 deletions src/api/qualicharge/auth/oidc.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
# API auth logger
logger = logging.getLogger(__name__)

auth_scheme = OAuth2PasswordBearer(tokenUrl=settings.OAUTH2_TOKEN_URL)
auth_scheme: Union[OAuth2PasswordBearer, HTTPBearer] = OAuth2PasswordBearer(
tokenUrl=settings.OAUTH2_TOKEN_URL
)
if settings.OIDC_IS_ENABLED:
auth_scheme = HTTPBearer()

Expand Down Expand Up @@ -123,7 +125,7 @@ def get_token(

try:
decoded_token = jwt.decode(
token=token.credentials if settings.OIDC_IS_ENABLED else token,
token=token.credentials if settings.OIDC_IS_ENABLED else token, # type: ignore[union-attr, arg-type]
key=key,
algorithms=algorithms,
**extra_decode_options, # type: ignore[arg-type]
Expand Down
2 changes: 1 addition & 1 deletion src/api/qualicharge/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def TEST_DATABASE_URL(self) -> PostgresDsn:
)

# OIDC
OIDC_IS_ENABLED: bool = False # If false, fallback to (local) OAuth2 scheme
OIDC_IS_ENABLED: bool = True # If false, fallback to (local) OAuth2 scheme
OIDC_PROVIDER_BASE_URL: AnyHttpUrl
OIDC_PROVIDER_DISCOVER_TIMEOUT: int = 5
OIDC_CONFIGURATION_PATH: Path = Path("/.well-known/openid-configuration")
Expand Down
94 changes: 93 additions & 1 deletion src/api/tests/api/v1/routers/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
from jose import jwt

from qualicharge.auth.factories import IDTokenFactory
from qualicharge.auth.models import UserRead
from qualicharge.auth.models import IDToken, UserCreate, UserRead
from qualicharge.auth.oidc import discover_provider, get_public_keys
from qualicharge.auth.schemas import User
from qualicharge.conf import settings


Expand Down Expand Up @@ -141,3 +142,94 @@ def test_whoami_jwt_decoding_error(
assert response.json() == {
"message": "Authentication failed: Unable to decode ID token"
}


def test_login_with_invalid_user(client):
"""Test the login endpoint with invalid user."""
response = client.post(
"/auth/token", data={"username": "johndoe", "password": "foo"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.json() == {
"message": "Authentication failed: Wrong login or password"
}


def test_login_with_invalid_password(client, db_session):
"""Test the login endpoint with invalid password."""
user = User(
**UserCreate(
username="johndoe",
password="foo", # noqa: S106
email="[email protected]",
first_name="John",
last_name="Doe",
is_superuser=False,
is_staff=False,
is_active=True,
).model_dump()
)
db_session.add(user)

response = client.post(
"/auth/token", data={"username": "johndoe", "password": "bar"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.json() == {
"message": "Authentication failed: Wrong login or password"
}


def test_login_with_inactive_user(client, db_session):
"""Test the login endpoint with an inactive user."""
user = User(
**UserCreate(
username="johndoe",
password="foo", # noqa: S106
email="[email protected]",
first_name="John",
last_name="Doe",
is_superuser=False,
is_staff=False,
is_active=False,
).model_dump()
)
db_session.add(user)

response = client.post(
"/auth/token", data={"username": "johndoe", "password": "foo"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.json() == {"message": "Authentication failed: User is not active"}


def test_login(client, db_session):
"""Test the login endpoint."""
user = User(
**UserCreate(
username="johndoe",
password="foo", # noqa: S106
email="[email protected]",
first_name="John",
last_name="Doe",
is_superuser=False,
is_staff=False,
is_active=True,
).model_dump()
)
db_session.add(user)

response = client.post(
"/auth/token", data={"username": "johndoe", "password": "foo"}
)
assert response.status_code == status.HTTP_200_OK
token = response.json()
assert token["token_type"] == "bearer" # noqa: S105
decoded = jwt.decode(
token=token["access_token"],
key=settings.OAUTH2_TOKEN_ENCODING_KEY,
audience=settings.OIDC_EXPECTED_AUDIENCE,
)
id_token = IDToken(**decoded)
assert id_token.sub == "johndoe"
assert id_token.email == "[email protected]"
112 changes: 67 additions & 45 deletions src/api/tests/auth/test_oidc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for qualicharge.auth.oidc module."""

from datetime import datetime
from typing import Union

import httpx
import pytest
Expand Down Expand Up @@ -92,65 +93,86 @@ def test_get_public_keys_with_bad_configuration(httpx_mock):
get_public_keys("http://oidc/wrong")


def test_get_token(httpx_mock, monkeypatch, id_token_factory: IDTokenFactory):
@pytest.mark.parametrize("oidc_is_enabled", (True, False))
def test_get_token(
oidc_is_enabled, httpx_mock, monkeypatch, id_token_factory: IDTokenFactory
):
"""Test the OIDC get token utility."""
monkeypatch.setenv("QUALICHARGE_OIDC_PROVIDER_BASE_URL", "http://oidc")
httpx_mock.add_response(
method="GET",
url=str(settings.OIDC_CONFIGURATION_URL),
json={
"jwks_uri": "https://oidc/certs",
"id_token_signing_alg_values_supported": "HS256",
},
)
httpx_mock.add_response(
method="GET",
url="https://oidc/certs",
json=[
"secret",
],
)
monkeypatch.setattr(settings, "OIDC_IS_ENABLED", oidc_is_enabled)
monkeypatch.setattr(settings, "OAUTH2_TOKEN_ENCODING_KEY", "secret")

if oidc_is_enabled:
httpx_mock.add_response(
method="GET",
url=str(settings.OIDC_CONFIGURATION_URL),
json={
"jwks_uri": "https://oidc/certs",
"id_token_signing_alg_values_supported": "HS256",
},
)
httpx_mock.add_response(
method="GET",
url="https://oidc/certs",
json=[
"secret",
],
)

bearer_token = HTTPAuthorizationCredentials(
scheme="Bearer",
credentials=jwt.encode(
claims=id_token_factory.build().model_dump(), key="secret"
),
token = jwt.encode(
claims=id_token_factory.build().model_dump(),
key="secret",
)
token = get_token(security_scopes=SecurityScopes(), token=bearer_token)
assert token.email == "[email protected]"
bearer_token: Union[str, HTTPAuthorizationCredentials] = token
if oidc_is_enabled:
bearer_token = HTTPAuthorizationCredentials(
scheme="Bearer",
credentials=token,
)

id_token = get_token(security_scopes=SecurityScopes(), token=bearer_token)
assert id_token.email == "[email protected]"


@pytest.mark.parametrize("oidc_is_enabled", (True, False))
def test_get_token_with_expired_token(
httpx_mock, monkeypatch, id_token_factory: IDTokenFactory
oidc_is_enabled, httpx_mock, monkeypatch, id_token_factory: IDTokenFactory
):
"""Test the OIDC get token utility when the token expired."""
monkeypatch.setenv("QUALICHARGE_OIDC_PROVIDER_BASE_URL", "http://oidc")
httpx_mock.add_response(
method="GET",
url=str(settings.OIDC_CONFIGURATION_URL),
json={
"jwks_uri": "https://oidc/certs",
"id_token_signing_alg_values_supported": "HS256",
},
)
httpx_mock.add_response(
method="GET",
url="https://oidc/certs",
json=[
"secret",
],
)
monkeypatch.setattr(settings, "OIDC_IS_ENABLED", oidc_is_enabled)
monkeypatch.setattr(settings, "OAUTH2_TOKEN_ENCODING_KEY", "secret")

if oidc_is_enabled:
httpx_mock.add_response(
method="GET",
url=str(settings.OIDC_CONFIGURATION_URL),
json={
"jwks_uri": "https://oidc/certs",
"id_token_signing_alg_values_supported": "HS256",
},
)
httpx_mock.add_response(
method="GET",
url="https://oidc/certs",
json=[
"secret",
],
)

# As exp should be set to iat + 300, the token should be expired
iat = int(datetime.now().timestamp()) - 500
bearer_token = HTTPAuthorizationCredentials(
scheme="Bearer",
credentials=jwt.encode(
claims=id_token_factory.build(iat=iat).model_dump(),
key="secret",
),
token = jwt.encode(
claims=id_token_factory.build(iat=iat).model_dump(),
key="secret",
)
bearer_token: Union[str, HTTPAuthorizationCredentials] = token
if oidc_is_enabled:
bearer_token = HTTPAuthorizationCredentials(
scheme="Bearer",
credentials=token,
)

with pytest.raises(OIDCAuthenticationError, match="Token signature expired"):
get_token(security_scopes=SecurityScopes(), token=bearer_token)

Expand Down
7 changes: 6 additions & 1 deletion src/api/tests/fixtures/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from qualicharge.auth.factories import GroupFactory, IDTokenFactory, UserFactory
from qualicharge.auth.oidc import get_token
from qualicharge.auth.schemas import UserGroup
from qualicharge.conf import settings


@pytest.fixture
Expand All @@ -17,7 +18,9 @@ def client():


@pytest.fixture
def client_auth(request, id_token_factory: IDTokenFactory, db_session: Session):
def client_auth(
request, id_token_factory: IDTokenFactory, db_session: Session, monkeypatch
):
"""An authenticated test client configured for the /api/v1 application.
Parameter:
Expand All @@ -33,6 +36,8 @@ def client_auth(request, id_token_factory: IDTokenFactory, db_session: Session):
GroupFactory.__session__ = db_session
UserFactory.__session__ = db_session

monkeypatch.setattr(settings, "OIDC_IS_ENABLED", True)

persist = True
fields = {
"email": "[email protected]",
Expand Down

0 comments on commit 9afaf3a

Please sign in to comment.