2026-06-13 16:32:33 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
def _seed_news_seo():
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.utils import generate_uid, make_combined_slug
|
|
|
|
|
|
|
|
|
|
uid = generate_uid()
|
|
|
|
|
title = f"SEO Test News Article {uid.split('-')[-1]}"
|
|
|
|
|
slug = make_combined_slug(title, uid)
|
|
|
|
|
get_table("news").insert(
|
|
|
|
|
{
|
2026-06-14 16:46:36 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-13 16:32:33 +02:00
|
|
|
"uid": uid,
|
|
|
|
|
"slug": slug,
|
|
|
|
|
"title": title,
|
|
|
|
|
"description": "A seeded news article for SEO tests.",
|
|
|
|
|
"content": "Body content for the seeded article.",
|
|
|
|
|
"url": "https://example.com/article",
|
|
|
|
|
"source_name": "ExampleSource",
|
|
|
|
|
"status": "published",
|
|
|
|
|
"synced_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
"show_on_landing": 0,
|
|
|
|
|
"grade": 8,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return slug, uid
|
|
|
|
|
def _seed_owner():
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"username": f"seo_{uid[:8]}",
|
|
|
|
|
"email": f"{uid[:8]}@seo.test",
|
|
|
|
|
"password_hash": "x",
|
|
|
|
|
"role": "Member",
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return uid
|
|
|
|
|
def _seed_post_seo(image=None):
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.utils import make_combined_slug
|
|
|
|
|
|
|
|
|
|
owner = _seed_owner()
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
slug = make_combined_slug("SEO Detail Post", uid)
|
|
|
|
|
get_table("posts").insert(
|
|
|
|
|
{
|
2026-06-14 16:46:36 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-13 16:32:33 +02:00
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": slug,
|
|
|
|
|
"title": "SEO Detail Post",
|
|
|
|
|
"content": "Body text for the SEO detail post.",
|
|
|
|
|
"topic": "general",
|
|
|
|
|
"project_uid": None,
|
|
|
|
|
"image": image,
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return slug, uid
|
|
|
|
|
def _seed_gist():
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.utils import make_combined_slug
|
|
|
|
|
|
|
|
|
|
owner = _seed_owner()
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
slug = make_combined_slug("SEO Detail Gist", uid)
|
|
|
|
|
get_table("gists").insert(
|
|
|
|
|
{
|
2026-06-14 16:46:36 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-13 16:32:33 +02:00
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": slug,
|
|
|
|
|
"title": "SEO Detail Gist",
|
|
|
|
|
"description": "Gist description for SEO tests.",
|
|
|
|
|
"source_code": "print('seo')",
|
|
|
|
|
"language": "python",
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return slug, uid
|
|
|
|
|
def _seed_project():
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.utils import make_combined_slug
|
|
|
|
|
|
|
|
|
|
owner = _seed_owner()
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
slug = make_combined_slug("SEO Detail Project", uid)
|
|
|
|
|
get_table("projects").insert(
|
|
|
|
|
{
|
2026-06-14 16:46:36 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-13 16:32:33 +02:00
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": slug,
|
|
|
|
|
"title": "SEO Detail Project",
|
|
|
|
|
"description": "Project description for SEO tests.",
|
|
|
|
|
"project_type": "software",
|
|
|
|
|
"platforms": "Linux",
|
|
|
|
|
"status": "Released",
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return slug, uid
|
|
|
|
|
def _seed_news_image(news_uid):
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
|
|
|
|
|
get_table("news_images").insert(
|
|
|
|
|
{
|
2026-06-14 16:46:36 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-13 16:32:33 +02:00
|
|
|
"news_uid": news_uid,
|
|
|
|
|
"url": "https://example.com/seo-news-image.jpg",
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
def _seed_feed_posts(count):
|
|
|
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.utils import make_combined_slug
|
|
|
|
|
|
|
|
|
|
owner = _seed_owner()
|
|
|
|
|
topic = f"seopag{owner[:8]}"
|
|
|
|
|
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
|
|
|
|
posts = get_table("posts")
|
|
|
|
|
for i in range(count):
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
posts.insert(
|
|
|
|
|
{
|
2026-06-14 16:46:36 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-13 16:32:33 +02:00
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": make_combined_slug(f"seo pag {i}", uid),
|
|
|
|
|
"title": None,
|
|
|
|
|
"content": f"seo pag post {i}",
|
|
|
|
|
"topic": topic,
|
|
|
|
|
"project_uid": None,
|
|
|
|
|
"image": None,
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": (base - timedelta(seconds=i)).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return topic
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_missing_profile_returns_404(app_server):
|
|
|
|
|
r = requests.get(f"{BASE_URL}/profile/no-such-user-xyz", allow_redirects=False)
|
|
|
|
|
assert r.status_code == 404
|
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_viewing_profile_marks_notification_read(app_server):
|
|
|
|
|
import time
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from devplacepy.database import get_table, refresh_snapshot
|
|
|
|
|
from devplacepy.utils import generate_uid
|
|
|
|
|
|
|
|
|
|
name = f"nmark{int(time.time() * 1000)}"
|
|
|
|
|
session = requests.Session()
|
|
|
|
|
session.post(
|
|
|
|
|
f"{BASE_URL}/auth/signup",
|
|
|
|
|
data={
|
|
|
|
|
"username": name,
|
|
|
|
|
"email": f"{name}@t.dev",
|
|
|
|
|
"password": "secret123",
|
|
|
|
|
"confirm_password": "secret123",
|
|
|
|
|
},
|
|
|
|
|
allow_redirects=True,
|
|
|
|
|
)
|
|
|
|
|
refresh_snapshot()
|
|
|
|
|
user = get_table("users").find_one(username=name)
|
|
|
|
|
notif_uid = generate_uid()
|
|
|
|
|
get_table("notifications").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": notif_uid,
|
|
|
|
|
"user_uid": user["uid"],
|
|
|
|
|
"type": "follow",
|
|
|
|
|
"message": "someone followed you",
|
|
|
|
|
"related_uid": generate_uid(),
|
|
|
|
|
"target_url": f"/profile/{name}",
|
|
|
|
|
"read": False,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
refresh_snapshot()
|
|
|
|
|
|
|
|
|
|
r = session.get(f"{BASE_URL}/profile/{name}", allow_redirects=True)
|
|
|
|
|
assert r.status_code == 200
|
|
|
|
|
|
|
|
|
|
refresh_snapshot()
|
|
|
|
|
assert bool(get_table("notifications").find_one(uid=notif_uid)["read"]) is True
|