import datetime
import threading
import uuid
import wsgiref.simple_server
from types import ModuleType
from typing import Any
import portpicker
import pytest
from canaille import create_app
from canaille.app import models
from canaille.core.models import Group
from canaille.core.models import User
from canaille.core.populate import fake_groups
from canaille.core.populate import fake_users
from canaille.oidc.basemodels import Token
from canaille.oidc.installation import generate_keypair
from flask import Flask
from flask import g
[docs]
class Server:
"""A proxy object that is returned by the pytest fixture."""
#: The port on which the local http server listens
port: int
#: The authorization server flask app
app: Flask
#: The module containing the available model classes
models: ModuleType
def __init__(self, app, port: int):
self.app = app
self.port = port
self.httpd = wsgiref.simple_server.make_server("localhost", port, app)
self.models = models
self.logged_user = None
@self.app.before_request
def logged_user():
if self.logged_user:
g.user = self.logged_user
@property
def url(self) -> str:
"""The URL at which the IAM server is accessible."""
return f"http://localhost:{self.port}/"
[docs]
def random_user(self, **kwargs) -> User:
"""Generates a :class:`~canaille.core.models.User` with random values.
Any parameter will be used instead of a random value.
"""
with self.app.app_context():
user = fake_users()[0]
user.update(**kwargs)
user.save()
return user
[docs]
def random_group(self, **kwargs) -> Group:
"""Generates a :class:`~canaille.core.models.Group` with random values.
Any parameter will be used instead of a random value.
"""
with self.app.app_context():
group = fake_groups(nb_users_max=0)[0]
group.update(**kwargs)
group.save()
return group
[docs]
def random_token(self, subject, client, **kwargs) -> Token:
"""Generates a test :class:`~canaille.oidc.basemodels.Token` with
random values.
Any parameter will be used instead of a random value.
"""
with self.app.app_context():
token = self.models.Token(
id=str(uuid.uuid4()),
token_id=str(uuid.uuid4()),
access_token=str(uuid.uuid4()),
client=client,
subject=subject,
type="access_token",
refresh_token=str(uuid.uuid4()),
scope=client.scope,
issue_date=datetime.datetime.now(datetime.timezone.utc),
lifetime=3600,
audience=[client],
)
token.update(**kwargs)
token.save()
return token
[docs]
def login(self, user):
"""Opens a session for the user in the IAM session.
This allows to skip the connection screen.
"""
self.logged_user = user
[docs]
def consent(self, user, client=None):
"""Make a user consent to share data with OIDC clients.
:param client: If :const:`None`, all existing clients are consented.
"""
with self.app.app_context():
clients = [client] if client else models.Client.query()
consents = [
self.models.Consent(
consent_id=str(uuid.uuid4()),
client=client,
subject=user,
scope=client.scope,
issue_date=datetime.datetime.now(datetime.timezone.utc),
)
for client in clients
]
for consent in consents:
consent.save()
if len(consents) > 1:
return consents
if len(consents) == 1:
return consents[0]
return None
[docs]
@pytest.fixture(scope="session")
def iam_configuration(tmp_path_factory) -> dict[str, Any]:
"""Fixture for editing the configuration of
:meth:`~pytest_iam.iam_server`."""
private_key, public_key = generate_keypair()
return {
"TESTING": True,
"ENV_FILE": None,
"SECRET_KEY": str(uuid.uuid4()),
"WTF_CSRF_ENABLED": False,
"CANAILLE": {
"JAVASCRIPT": False,
"ACL": {
"DEFAULT": {
"PERMISSIONS": ["use_oidc", "manage_oidc"],
}
},
"LOGGING": {
"version": 1,
"formatters": {
"default": {
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
}
},
"handlers": {
"canaille": {
"class": "logging.NullHandler",
"formatter": "default",
}
},
"root": {"level": "DEBUG", "handlers": ["canaille"]},
},
},
"CANAILLE_OIDC": {
"DYNAMIC_CLIENT_REGISTRATION_OPEN": True,
"JWT": {
"PUBLIC_KEY": public_key,
"PRIVATE_KEY": private_key,
},
},
}
[docs]
@pytest.fixture(scope="session")
def iam_server(iam_configuration) -> Server:
"""Fixture that creates a Canaille server listening a random port in a
thread."""
port = portpicker.pick_unused_port()
app = create_app(
config=iam_configuration, env_file=".pytest-iam.env", env_prefix="PYTEST_IAM_"
)
server = Server(app, port)
server_thread = threading.Thread(target=server.httpd.serve_forever)
server_thread.start()
try:
with app.app_context():
yield server
finally:
server.httpd.shutdown()
server_thread.join()