From fd8c8cbf39e17bdb337eab3e860357c00d7bc769 Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 12 Feb 2026 14:56:20 +0100 Subject: [PATCH] feat: add repository Protocol interfaces for User, Credential, MagicLink --- src/fastapi_oidc_op/store/protocols.py | 53 ++++++++++++++++++++++++++ tests/test_store/test_protocols.py | 13 +++++++ 2 files changed, 66 insertions(+) create mode 100644 src/fastapi_oidc_op/store/protocols.py create mode 100644 tests/test_store/test_protocols.py diff --git a/src/fastapi_oidc_op/store/protocols.py b/src/fastapi_oidc_op/store/protocols.py new file mode 100644 index 0000000..d60dc01 --- /dev/null +++ b/src/fastapi_oidc_op/store/protocols.py @@ -0,0 +1,53 @@ +from typing import Protocol, runtime_checkable + +from fastapi_oidc_op.models import ( + MagicLink, + PasswordCredential, + User, + WebAuthnCredential, +) + + +@runtime_checkable +class UserRepository(Protocol): + async def create(self, user: User) -> User: ... + + async def get_by_userid(self, userid: str) -> User | None: ... + + async def get_by_username(self, username: str) -> User | None: ... + + async def update(self, user: User) -> User: ... + + async def list_users(self, offset: int = 0, limit: int = 100) -> list[User]: ... + + async def delete(self, userid: str) -> bool: ... + + +@runtime_checkable +class CredentialRepository(Protocol): + async def create_webauthn(self, credential: WebAuthnCredential) -> WebAuthnCredential: ... + + async def create_password(self, credential: PasswordCredential) -> PasswordCredential: ... + + async def get_webauthn_by_user(self, user_id: str) -> list[WebAuthnCredential]: ... + + async def get_webauthn_by_credential_id(self, credential_id: bytes) -> WebAuthnCredential | None: ... + + async def get_password_by_user(self, user_id: str) -> PasswordCredential | None: ... + + async def update_webauthn(self, credential: WebAuthnCredential) -> WebAuthnCredential: ... + + async def delete_webauthn(self, user_id: str, credential_id: bytes) -> bool: ... + + async def delete_password(self, user_id: str) -> bool: ... + + +@runtime_checkable +class MagicLinkRepository(Protocol): + async def create(self, link: MagicLink) -> MagicLink: ... + + async def get_by_token(self, token: str) -> MagicLink | None: ... + + async def mark_used(self, token: str) -> bool: ... + + async def delete_expired(self) -> int: ... diff --git a/tests/test_store/test_protocols.py b/tests/test_store/test_protocols.py new file mode 100644 index 0000000..4b87115 --- /dev/null +++ b/tests/test_store/test_protocols.py @@ -0,0 +1,13 @@ +from typing import runtime_checkable + +from fastapi_oidc_op.store.protocols import ( + CredentialRepository, + MagicLinkRepository, + UserRepository, +) + + +def test_protocols_are_runtime_checkable() -> None: + assert runtime_checkable(UserRepository) # type: ignore[arg-type] + assert runtime_checkable(CredentialRepository) # type: ignore[arg-type] + assert runtime_checkable(MagicLinkRepository) # type: ignore[arg-type]