"""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)