feat: ordre naturel des sections Configurations et avatar utilisateur #113
CI / lint (push) Successful in 1m59s
CI / security (push) Successful in 1m28s
CI / test (push) Successful in 4m14s
CI / build (push) Successful in 1m20s
CI / e2e (push) Successful in 13m43s

This commit is contained in:
2026-09-23 22:18:29 -04:00
parent a726ad8511
commit 33fe1a3439
24 changed files with 1078 additions and 296 deletions
+95 -1
View File
@@ -308,4 +308,98 @@ class TestAdmin:
resp = auth_client.post("/api/auth/logout", headers={
"Authorization": f"Bearer {token}",
})
assert resp.status_code == 200
assert resp.status_code == 200
# ═══════════════════════════════════════════════════════════════════
# Profile avatar (#113)
# ═══════════════════════════════════════════════════════════════════
# 1×1 transparent PNG.
TINY_PNG = (
"data:image/png;base64,"
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8"
"z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
)
class TestAvatar:
def _login(self, client):
resp = client.post("/api/auth/login", json={
"username": "admin", "password": "chab30",
})
return resp.json()["access_token"]
def _headers(self, token):
return {"Authorization": f"Bearer {token}"}
def test_me_returns_avatar_field(self, auth_client):
"""GET /api/auth/me exposes the avatar key (None by default)."""
token = self._login(auth_client)
resp = auth_client.get("/api/auth/me", headers=self._headers(token))
assert resp.status_code == 200
assert "avatar" in resp.json()
assert resp.json()["avatar"] is None
def test_patch_me_sets_avatar(self, auth_client):
"""A valid PNG data-URL is persisted and returned by GET /me."""
token = self._login(auth_client)
resp = auth_client.patch("/api/auth/me", headers=self._headers(token),
json={"avatar": TINY_PNG})
assert resp.status_code == 200
assert resp.json()["avatar"] == TINY_PNG
me = auth_client.get("/api/auth/me", headers=self._headers(token)).json()
assert me["avatar"] == TINY_PNG
def test_patch_me_avatar_empty_string_clears(self, auth_client):
"""Sending an empty string removes the avatar."""
token = self._login(auth_client)
auth_client.patch("/api/auth/me", headers=self._headers(token),
json={"avatar": TINY_PNG})
resp = auth_client.patch("/api/auth/me", headers=self._headers(token),
json={"avatar": ""})
assert resp.status_code == 200
assert resp.json()["avatar"] is None
def test_patch_me_avatar_rejects_svg(self, auth_client):
"""SVG data-URLs are refused (XSS surface)."""
token = self._login(auth_client)
svg = "data:image/svg+xml;base64,PHN2Zz48L3N2Zz4="
resp = auth_client.patch("/api/auth/me", headers=self._headers(token),
json={"avatar": svg})
assert resp.status_code == 400
def test_patch_me_avatar_rejects_non_image_payload(self, auth_client):
"""A PNG data-URL header over non-image bytes is refused."""
token = self._login(auth_client)
fake = "data:image/png;base64,aGVsbG8gd29ybGQ=" # "hello world"
resp = auth_client.patch("/api/auth/me", headers=self._headers(token),
json={"avatar": fake})
assert resp.status_code == 400
def test_patch_me_avatar_rejects_too_large(self, auth_client):
"""Payloads above the size cap are refused before any decode."""
token = self._login(auth_client)
huge = "data:image/png;base64," + "A" * 500_000
resp = auth_client.patch("/api/auth/me", headers=self._headers(token),
json={"avatar": huge})
assert resp.status_code == 400
def test_patch_me_avatar_rejects_invalid_base64(self, auth_client):
"""Malformed base64 inside a well-formed data-URL is refused."""
token = self._login(auth_client)
resp = auth_client.patch("/api/auth/me", headers=self._headers(token),
json={"avatar": "data:image/png;base64,@@not-base64@@"})
assert resp.status_code == 400
def test_login_user_payload_includes_avatar(self, auth_client):
"""The login response user object carries the avatar for the sidebar."""
token = self._login(auth_client)
auth_client.patch("/api/auth/me", headers=self._headers(token),
json={"avatar": TINY_PNG})
# Re-login to get a fresh user payload.
resp = auth_client.post("/api/auth/login", json={
"username": "admin", "password": "chab30",
})
assert resp.status_code == 200
assert resp.json()["user"]["avatar"] == TINY_PNG