2026-06-13 16:32:33 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
feat: replace raw form parsing with pydantic models for auth, admin, bugs, comments, and gists
Migrate all route handlers from manual `request.form()` parsing to typed `Annotated[Model, Form()]` dependencies using new pydantic models (SignupForm, LoginForm, ForgotPasswordForm, AdminRoleForm, AdminPasswordForm, BugForm, CommentForm, GistForm). Remove inline validation logic from signup, login, password reset, admin role/password change, bug creation, comment creation, and gist creation endpoints, delegating validation to model validators and field constraints. Add `RequestValidationError` exception handler with friendly error messages for auth form pages. Update model definitions to use `field_validator` and `model_validator` for cross-field checks like password matching.
2026-05-23 05:44:04 +02:00
|
|
|
import requests
|
feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
2026-06-28 00:31:34 +02:00
|
|
|
from devplacepy.avatar import avatar_seed, avatar_url, generate_avatar_svg
|
feat: replace raw form parsing with pydantic models for auth, admin, bugs, comments, and gists
Migrate all route handlers from manual `request.form()` parsing to typed `Annotated[Model, Form()]` dependencies using new pydantic models (SignupForm, LoginForm, ForgotPasswordForm, AdminRoleForm, AdminPasswordForm, BugForm, CommentForm, GistForm). Remove inline validation logic from signup, login, password reset, admin role/password change, bug creation, comment creation, and gist creation endpoints, delegating validation to model validators and field constraints. Add `RequestValidationError` exception handler with friendly error messages for auth form pages. Update model definitions to use `field_validator` and `model_validator` for cross-field checks like password matching.
2026-05-23 05:44:04 +02:00
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 03:14:43 +02:00
|
|
|
def test_avatar_url_format():
|
|
|
|
|
url = avatar_url("multiavatar", "testuser", 128)
|
|
|
|
|
assert url == "/avatar/multiavatar/testuser?size=128"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_avatar_url_different_size():
|
|
|
|
|
url = avatar_url("multiavatar", "testuser", 64)
|
|
|
|
|
assert "size=64" in url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_avatar_url_contains_seed():
|
|
|
|
|
url = avatar_url("multiavatar", "customuser", 128)
|
|
|
|
|
assert "customuser" in url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_generate_avatar_svg_returns_string():
|
|
|
|
|
svg = generate_avatar_svg("testuser")
|
|
|
|
|
assert isinstance(svg, str)
|
|
|
|
|
assert len(svg) > 100
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_generate_avatar_svg_is_svg():
|
|
|
|
|
svg = generate_avatar_svg("testuser")
|
|
|
|
|
assert svg.strip().startswith("<svg")
|
|
|
|
|
assert "xmlns" in svg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_generate_avatar_svg_deterministic():
|
|
|
|
|
svg1 = generate_avatar_svg("testuser")
|
|
|
|
|
svg2 = generate_avatar_svg("testuser")
|
|
|
|
|
assert svg1 == svg2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_generate_avatar_svg_different_for_different_seeds():
|
|
|
|
|
svg1 = generate_avatar_svg("alice")
|
|
|
|
|
svg2 = generate_avatar_svg("bob")
|
|
|
|
|
assert svg1 != svg2
|
|
|
|
|
|
|
|
|
|
|
2026-06-10 05:22:44 +02:00
|
|
|
def test_generate_avatar_svg_fallback_on_exception(monkeypatch):
|
2026-06-12 05:37:12 +02:00
|
|
|
import importlib
|
|
|
|
|
|
|
|
|
|
ma = importlib.import_module("multiavatar.multiavatar")
|
2026-06-10 05:22:44 +02:00
|
|
|
|
|
|
|
|
def _boom(*args, **kwargs):
|
|
|
|
|
raise RuntimeError("avatar backend down")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(ma, "multiavatar", _boom)
|
|
|
|
|
svg = generate_avatar_svg("testuser")
|
|
|
|
|
assert svg.strip().startswith("<svg")
|
|
|
|
|
assert "<rect" in svg
|
|
|
|
|
assert ">T</text>" in svg
|
|
|
|
|
|
2026-05-11 03:14:43 +02:00
|
|
|
|
2026-06-10 05:22:44 +02:00
|
|
|
def test_generate_avatar_svg_includes_rect_for_fallback(monkeypatch):
|
2026-06-12 05:37:12 +02:00
|
|
|
import importlib
|
|
|
|
|
|
|
|
|
|
ma = importlib.import_module("multiavatar.multiavatar")
|
2026-05-11 03:14:43 +02:00
|
|
|
|
2026-06-10 05:22:44 +02:00
|
|
|
monkeypatch.setattr(ma, "multiavatar", lambda *a, **k: None)
|
|
|
|
|
svg = generate_avatar_svg("zeta")
|
|
|
|
|
assert "<rect" in svg
|
|
|
|
|
assert "<text" in svg
|
2026-05-11 03:14:43 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_avatar_url_with_different_seeds():
|
|
|
|
|
url1 = avatar_url("multiavatar", "alice", 128)
|
|
|
|
|
url2 = avatar_url("multiavatar", "bob", 128)
|
|
|
|
|
assert url1 != url2
|
|
|
|
|
|
|
|
|
|
|
feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
2026-06-28 00:31:34 +02:00
|
|
|
def test_avatar_seed_prefers_avatar_seed_field():
|
|
|
|
|
assert avatar_seed({"username": "alice", "avatar_seed": "seed-xyz"}) == "seed-xyz"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_avatar_seed_falls_back_to_username():
|
|
|
|
|
assert avatar_seed({"username": "alice"}) == "alice"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_avatar_seed_falls_back_when_seed_blank():
|
|
|
|
|
assert avatar_seed({"username": "alice", "avatar_seed": ""}) == "alice"
|
|
|
|
|
assert avatar_seed({"username": "alice", "avatar_seed": None}) == "alice"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_avatar_seed_none_user():
|
|
|
|
|
assert avatar_seed(None) == ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_avatar_seed_empty_user():
|
|
|
|
|
assert avatar_seed({}) == ""
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 03:14:43 +02:00
|
|
|
def test_generate_avatar_svg_empty_seed_uses_fallback():
|
|
|
|
|
svg = generate_avatar_svg("")
|
|
|
|
|
assert isinstance(svg, str)
|
2026-06-10 05:22:44 +02:00
|
|
|
assert "<rect" in svg
|