fix: add collision retry for userid generation
This commit is contained in:
parent
e4e484dc4b
commit
9d7a67b2d2
2 changed files with 60 additions and 1 deletions
|
|
@ -2,6 +2,8 @@ import secrets
|
|||
|
||||
from proquint import uint2quint
|
||||
|
||||
from fastapi_oidc_op.store.protocols import UserRepository
|
||||
|
||||
|
||||
def generate_userid() -> str:
|
||||
"""Generate a unique user identifier in proquint format.
|
||||
|
|
@ -9,3 +11,17 @@ def generate_userid() -> str:
|
|||
Returns a 32-bit proquint string like 'lusab-bansen'.
|
||||
"""
|
||||
return uint2quint(secrets.randbits(32))
|
||||
|
||||
|
||||
async def generate_unique_userid(user_repo: UserRepository, max_retries: int = 10) -> str:
|
||||
"""Generate a userid that does not already exist in the repository.
|
||||
|
||||
Calls generate_userid() and checks for collisions via user_repo.get_by_userid().
|
||||
Retries up to max_retries times before raising RuntimeError.
|
||||
"""
|
||||
for _ in range(max_retries):
|
||||
candidate = generate_userid()
|
||||
existing = await user_repo.get_by_userid(candidate)
|
||||
if existing is None:
|
||||
return candidate
|
||||
raise RuntimeError(f"Failed to generate unique userid after {max_retries} retries")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
from fastapi_oidc_op.userid import generate_userid
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from fastapi_oidc_op.models import User
|
||||
from fastapi_oidc_op.userid import generate_unique_userid, generate_userid
|
||||
|
||||
|
||||
def test_generate_userid_format() -> None:
|
||||
|
|
@ -18,3 +23,41 @@ def test_generate_userid_uniqueness() -> None:
|
|||
def test_generate_userid_is_lowercase() -> None:
|
||||
userid = generate_userid()
|
||||
assert userid == userid.lower()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_generate_unique_userid_no_collision() -> None:
|
||||
"""Returns a userid when no collision occurs."""
|
||||
user_repo = AsyncMock()
|
||||
user_repo.get_by_userid.return_value = None
|
||||
|
||||
result = await generate_unique_userid(user_repo)
|
||||
|
||||
parts = result.split("-")
|
||||
assert len(parts) == 2
|
||||
user_repo.get_by_userid.assert_called_once_with(result)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_generate_unique_userid_retries_on_collision() -> None:
|
||||
"""Retries when a collision is detected and succeeds on second attempt."""
|
||||
existing_user = User(userid="taken-xxxxx", username="existing")
|
||||
user_repo = AsyncMock()
|
||||
user_repo.get_by_userid.side_effect = [existing_user, None]
|
||||
|
||||
result = await generate_unique_userid(user_repo)
|
||||
|
||||
assert user_repo.get_by_userid.call_count == 2
|
||||
parts = result.split("-")
|
||||
assert len(parts) == 2
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_generate_unique_userid_raises_after_max_retries() -> None:
|
||||
"""Raises RuntimeError when max retries are exhausted."""
|
||||
existing_user = User(userid="taken-xxxxx", username="existing")
|
||||
user_repo = AsyncMock()
|
||||
user_repo.get_by_userid.return_value = existing_user
|
||||
|
||||
with pytest.raises(RuntimeError, match="Failed to generate unique userid"):
|
||||
await generate_unique_userid(user_repo)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue