Files
webmail-client/backend/tests/api/test_avatar.py
T

86 lines
2.8 KiB
Python
Raw Normal View History

"""Sender avatar cache (SPEC §9.1): a cached hit or miss must be served again.
The shared Redis client decodes replies as text, so the cache of raw image
bytes broke on the second request for any sender — a 500 instead of the image
(or instead of the 404 that means "draw initials").
"""
from __future__ import annotations
import uuid
from collections.abc import AsyncIterator
import httpx
import pytest
import pytest_asyncio
from httpx import ASGITransport
from sqlalchemy import delete
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
from app.api import avatar
from app.config import get_settings
from app.identity.passwords import hash_password
from app.identity.sessions import create_session
from app.main import create_app
from app.models import User
# A PNG signature: not valid UTF-8, exactly what a text-decoding client chokes on.
PNG = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\xff\xfe"
@pytest_asyncio.fixture
async def client() -> AsyncIterator[httpx.AsyncClient]:
engine = create_async_engine(get_settings().database_url, poolclass=NullPool)
session = async_sessionmaker(engine, expire_on_commit=False)()
user = User(handle=f"av{uuid.uuid4().hex[:8]}@test.local", password_hash=hash_password("pw"))
session.add(user)
await session.commit()
token = await create_session(user.id, "pytest")
async with httpx.AsyncClient(
transport=ASGITransport(app=create_app()),
base_url="http://testserver",
cookies={"session": token},
) as c:
yield c
await session.execute(delete(User).where(User.id == user.id))
await session.commit()
await session.close()
await engine.dispose()
async def test_cached_avatar_is_served_again(
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
calls = 0
async def fake_resolve(email: str, md5: str) -> tuple[str, bytes] | None:
nonlocal calls
calls += 1
return "image/png", PNG
monkeypatch.setattr(avatar, "_resolve", fake_resolve)
for _ in range(2):
resp = await client.get("/api/avatar", params={"email": "alice@example.com"})
assert resp.status_code == 200
assert resp.content == PNG
assert resp.headers["content-type"] == "image/png"
assert calls == 1 # the second came from the cache
async def test_cached_miss_is_a_404_again(
client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
) -> None:
calls = 0
async def fake_resolve(email: str, md5: str) -> tuple[str, bytes] | None:
nonlocal calls
calls += 1
return None
monkeypatch.setattr(avatar, "_resolve", fake_resolve)
for _ in range(2):
resp = await client.get("/api/avatar", params={"email": "nobody@example.com"})
assert resp.status_code == 404
assert calls == 1