feat: add Pydantic models for User, Credential, and MagicLink

This commit is contained in:
Johan Lundberg 2026-02-12 14:50:32 +01:00
parent 16a78663f3
commit e5220c97b1
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
2 changed files with 141 additions and 0 deletions

View file

@ -0,0 +1,62 @@
from datetime import UTC, datetime, timedelta
from enum import StrEnum
from pydantic import BaseModel, Field
def _utcnow() -> datetime:
return datetime.now(UTC)
def _default_expiry() -> datetime:
return datetime.now(UTC) + timedelta(hours=24)
class CredentialType(StrEnum):
WEBAUTHN = "webauthn"
PASSWORD = "password"
class User(BaseModel):
userid: str
username: str
preferred_username: str | None = None
given_name: str | None = None
family_name: str | None = None
nickname: str | None = None
email: str | None = None
email_verified: bool = False
phone_number: str | None = None
phone_number_verified: bool = False
picture: str | None = None
locale: str | None = None
active: bool = True
created_at: datetime = Field(default_factory=_utcnow)
updated_at: datetime = Field(default_factory=_utcnow)
groups: list[str] = Field(default_factory=list)
class WebAuthnCredential(BaseModel):
user_id: str
type: CredentialType = CredentialType.WEBAUTHN
credential_id: bytes
public_key: bytes
sign_count: int = 0
device_name: str = ""
created_at: datetime = Field(default_factory=_utcnow)
class PasswordCredential(BaseModel):
user_id: str
type: CredentialType = CredentialType.PASSWORD
password_hash: str
created_at: datetime = Field(default_factory=_utcnow)
class MagicLink(BaseModel):
token: str
username: str
expires_at: datetime = Field(default_factory=_default_expiry)
used: bool = False
created_by: str | None = None
note: str | None = None

79
tests/test_models.py Normal file
View file

@ -0,0 +1,79 @@
from datetime import UTC, datetime
from fastapi_oidc_op.models import (
CredentialType,
MagicLink,
PasswordCredential,
User,
WebAuthnCredential,
)
def test_user_creation() -> None:
user = User(
userid="lusab-bansen",
username="alice",
)
assert user.userid == "lusab-bansen"
assert user.username == "alice"
assert user.preferred_username is None
assert user.email is None
assert user.active is True
assert user.groups == []
assert user.created_at is not None
assert user.updated_at is not None
def test_user_with_all_fields() -> None:
user = User(
userid="lusab-bansen",
username="alice",
preferred_username="Alice W.",
given_name="Alice",
family_name="Wonderland",
nickname="ally",
email="alice@example.com",
email_verified=True,
phone_number="+1234567890",
phone_number_verified=False,
picture="https://example.com/alice.jpg",
locale="en-US",
active=True,
groups=["admin", "users"],
)
assert user.given_name == "Alice"
assert user.groups == ["admin", "users"]
def test_webauthn_credential() -> None:
cred = WebAuthnCredential(
user_id="lusab-bansen",
credential_id=b"\x01\x02\x03",
public_key=b"\x04\x05\x06",
sign_count=0,
device_name="YubiKey 5",
)
assert cred.type == CredentialType.WEBAUTHN
assert cred.credential_id == b"\x01\x02\x03"
assert cred.device_name == "YubiKey 5"
def test_password_credential() -> None:
cred = PasswordCredential(
user_id="lusab-bansen",
password_hash="$argon2id$v=19$m=65536,t=3,p=4$hash",
)
assert cred.type == CredentialType.PASSWORD
assert cred.password_hash.startswith("$argon2")
def test_magic_link() -> None:
link = MagicLink(
token="abc123def456",
username="newuser",
)
assert link.token == "abc123def456"
assert link.username == "newuser"
assert link.used is False
assert link.created_by is None
assert link.expires_at > datetime.now(UTC)