Short IDs become the canonical identifier in URLs (/i/:short_id), MinIO/R2 storage keys, and all API responses. Hash-based deduplication is preserved. Includes two-phase Alembic migration (003 adds nullable column, 004 enforces NOT NULL) with a backfill script to copy storage objects and populate short_id for existing images. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
60 lines
1.8 KiB
Python
60 lines
1.8 KiB
Python
"""Unit tests for short_id generation, validation, and repository lookup."""
|
|
|
|
import re
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from app.routers.images import _validate_short_id
|
|
from app.utils import generate_short_id
|
|
|
|
_SHORT_ID_RE = re.compile(r"^[a-zA-Z0-9]{8}$")
|
|
|
|
|
|
def test_validate_short_id_accepts_valid():
|
|
_validate_short_id("AbCd1234") # must not raise
|
|
|
|
|
|
def test_validate_short_id_rejects_too_long():
|
|
with pytest.raises(HTTPException) as exc:
|
|
_validate_short_id("toolong!!")
|
|
assert exc.value.status_code == 422
|
|
|
|
|
|
def test_validate_short_id_rejects_too_short():
|
|
with pytest.raises(HTTPException) as exc:
|
|
_validate_short_id("short")
|
|
assert exc.value.status_code == 422
|
|
|
|
|
|
def test_validate_short_id_rejects_invalid_chars():
|
|
with pytest.raises(HTTPException) as exc:
|
|
_validate_short_id("has spa!")
|
|
assert exc.value.status_code == 422
|
|
|
|
|
|
def test_generate_short_id_unique():
|
|
ids = {generate_short_id() for _ in range(100)}
|
|
assert len(ids) > 90 # collision in 100 draws would be astronomically unlikely
|
|
|
|
|
|
def test_repo_get_by_short_id_uses_correct_field():
|
|
"""get_by_short_id selects on Image.short_id, not Image.id."""
|
|
import asyncio
|
|
|
|
from app.repositories.image_repo import ImageRepository
|
|
|
|
mock_session = MagicMock()
|
|
scalar = MagicMock()
|
|
scalar.scalar_one_or_none = MagicMock(return_value=None)
|
|
mock_session.execute = AsyncMock(return_value=scalar)
|
|
|
|
repo = ImageRepository(mock_session)
|
|
asyncio.get_event_loop().run_until_complete(repo.get_by_short_id("AbCd1234"))
|
|
|
|
call_args = mock_session.execute.call_args[0][0]
|
|
compiled = call_args.compile(compile_kwargs={"literal_binds": True})
|
|
assert "short_id" in str(compiled)
|
|
assert "AbCd1234" in str(compiled)
|