873 lines
30 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
import base64
import time
import requests
from tests.conftest import BASE_URL
from devplacepy.database import get_table
_counter_api_auth = [0]
def _signup_api_auth(password="secret123"):
_counter_api_auth[0] += 1
name = f"apiauth{int(time.time() * 1000)}{_counter_api_auth[0]}"
email = f"{name}@t.dev"
session = requests.Session()
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": email,
"password": password,
"confirm_password": password,
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
return session, name, email, password
def _user_api_auth(name):
return get_table("users").find_one(username=name)
def _key_api_auth(name):
return _user_api_auth(name)["api_key"]
from playwright.sync_api import expect
from devplacepy.database import (
CUSTOMIZATION_GLOBAL_SCOPE,
get_customization_prefs,
get_table,
list_custom_overrides,
set_custom_override,
set_customization_pref,
)
from devplacepy.utils import clear_user_cache
def _user_customization_toggle(username):
return get_table("users").find_one(username=username)
def _seed_global_css(uid, sentinel):
set_custom_override(
"user", uid, CUSTOMIZATION_GLOBAL_SCOPE, "css", f".{sentinel} {{ color: red; }}"
)
def _profile(page, username):
page.goto(f"{BASE_URL}/profile/{username}", wait_until="domcontentloaded")
def _click_toggle(page, category):
page.locator(f"[data-customization-toggle='{category}']").click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
import re
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from tests.conftest import BASE_URL, assert_share_copies
from devplacepy.utils import make_combined_slug
DPBOT_SIZE = 112147
SOURCE_LENGTH_LIMIT = 400000
def _make_source(length):
unit = "def handler():\n return 0\n"
return (unit * (length // len(unit) + 1))[:length]
def _register_session(prefix):
name = f"{prefix}{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",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
return session, name
def _seed_gists(count):
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"pag_{owner[:8]}",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"terms_version": "1",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
gists = get_table("gists")
for i in range(count):
uid = str(uuid4())
title = f"Pag Gist {i}"
gists.insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(title, uid),
"title": title,
"language": "python",
"description": "paginated",
"stars": 0,
"created_at": (base - timedelta(seconds=i)).isoformat(),
}
)
return owner
def _set_cm_value(page, value):
page.evaluate(f"""() => {{
const cm = document.querySelector(".CodeMirror")?.CodeMirror;
if (cm) {{
cm.setValue({repr(value)});
cm.save();
}}
}}""")
def _create_gist(
page,
title="Test Gist",
description="Test description",
language="python",
source_code="print('hello')",
):
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
page.locator("#create-gist-btn").wait_for(state="visible", timeout=10000)
page.locator("#create-gist-btn").click()
page.wait_for_timeout(800)
page.fill("#gist-title", title)
page.fill("#gist-description", description)
page.select_option("#gist-language", language)
page.wait_for_timeout(300)
_set_cm_value(page, source_code)
page.wait_for_timeout(300)
page.locator("button.btn-primary:has-text('Create Gist')").click()
page.wait_for_timeout(2000)
current = page.url
if "/gists/" in current and current != f"{BASE_URL}/gists":
return
page.wait_for_url("**/gists/*", timeout=15000, wait_until="domcontentloaded")
def _seed_searchable_gists(language="python"):
token = uuid4().hex[:10]
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"gsrch_{owner[:8]}",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"terms_version": "1",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
gists = get_table("gists")
match_title = f"Findable gist {token}"
other_title = f"Unrelated gist {uuid4().hex[:10]}"
for offset, title in ((0, match_title), (1, other_title)):
uid = str(uuid4())
gists.insert(
{
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(title, uid),
"title": title,
"language": language,
"description": f"Description for {title}",
"source_code": "x = 1",
"stars": 0,
"created_at": (base - timedelta(seconds=offset)).isoformat(),
"deleted_at": None,
"deleted_by": None,
}
)
return token, match_title, other_title
import io
import uuid
from PIL import Image
JSON_media = {"Accept": "application/json"}
def _png_bytes_media(color=(200, 30, 30)):
buf = io.BytesIO()
Image.new("RGB", (8, 8), color).save(buf, "PNG")
return buf.getvalue()
def _signup_media(prefix="media"):
s = requests.Session()
name = f"{prefix}_{uuid.uuid4().hex[:10]}"
s.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
return s, name
def _login_media(seeded_db, who):
s = requests.Session()
creds = seeded_db[who]
s.post(
f"{BASE_URL}/auth/login",
data={"email": creds["email"], "password": creds["password"]},
allow_redirects=True,
)
return s
def _upload_media(s, name="pic.png", content=None, mime="image/png"):
files = {"file": (name, content if content is not None else _png_bytes_media(), mime)}
r = s.post(f"{BASE_URL}/uploads/upload", files=files)
assert r.status_code == 201, r.text
return r.json()["uid"]
def _create_post_media(s, attachment_uids, title="media post"):
r = s.post(
f"{BASE_URL}/posts/create",
data={
"content": "A post that carries media for the gallery tests.",
"title": title,
"topic": "random",
"attachment_uids": attachment_uids,
},
allow_redirects=True,
)
assert r.status_code == 200, r.text[:300]
assert "/posts/" in r.url, r.url
return r.url
def _create_project_media(s, attachment_uids, title="Media Project"):
r = s.post(
f"{BASE_URL}/projects/create",
data={
"title": title,
"description": "Project with media for gallery tests.",
"project_type": "software",
"platforms": "linux",
"status": "In Development",
"attachment_uids": attachment_uids,
},
allow_redirects=True,
)
assert r.status_code == 200, r.text[:300]
assert "/projects/" in r.url, r.url
return r.url
def _media_json(session, username):
r = session.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
assert r.status_code == 200, r.text[:300]
return r.json()
def _media_uids(session, username):
return [m["uid"] for m in _media_json(session, username)["media"]]
def _seed_media_via_browser(page, title="ui media"):
up = page.request.post(
f"{BASE_URL}/uploads/upload",
multipart={
"file": {
"name": "pic.png",
"mimeType": "image/png",
"buffer": _png_bytes_media(),
}
},
)
assert up.ok, up.text()
uid = up.json()["uid"]
post = page.request.post(
f"{BASE_URL}/posts/create",
form={
"content": "media for the ui gallery test",
"title": title,
"topic": "random",
"attachment_uids": uid,
},
)
assert post.ok, post.text()
return uid
def _media_uids_via_page(page, username):
resp = page.request.get(f"{BASE_URL}/profile/{username}?tab=media", headers=JSON_media)
return [m["uid"] for m in resp.json()["media"]]
from devplacepy.database import (
get_notification_default,
get_notification_prefs,
get_table,
notification_enabled,
reset_notification_prefs,
set_notification_default,
set_notification_pref,
)
from devplacepy.utils import create_notification, generate_uid
def _user_notification_prefs(username):
return get_table("users").find_one(username=username)
def _pref(prefs, key):
return next(p for p in prefs if p["key"] == key)
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(
{
"deleted_at": None,
"deleted_by": None,
"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]}",
Add the trust and safety subsystem and the App Store compliance work Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
2026-08-09 00:18:20 +02:00
"terms_version": "1",
"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(
{
"deleted_at": None,
"deleted_by": None,
"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(
{
"deleted_at": None,
"deleted_by": None,
"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(
{
"deleted_at": None,
"deleted_by": None,
"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(
{
"deleted_at": None,
"deleted_by": None,
"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(
{
"deleted_at": None,
"deleted_by": None,
"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_admin_sees_other_users_key(alice):
page, _ = alice
_, member, _, _ = _signup_api_auth()
page.goto(f"{BASE_URL}/profile/{member}", wait_until="domcontentloaded")
assert page.locator("[data-api-key-card]").count() == 1
assert _key_api_auth(member) in page.content()
def test_non_owner_non_admin_hidden_and_guarded(bob):
page, user = bob
member = _user_customization_toggle(user["username"])
get_table("users").update({"uid": member["uid"], "role": "Member"}, ["uid"])
clear_user_cache(member["uid"])
target = _user_customization_toggle("alice_test")
_profile(page, target["username"])
assert page.locator("[data-customization]").count() == 0
response = requests.post(
f"{BASE_URL}/profile/{target['username']}/customization/global",
data={"value": "1"},
headers={"Accept": "application/json", "X-API-KEY": member["api_key"]},
allow_redirects=False,
)
assert response.status_code == 403
def test_profile_gists_tab(alice, app_server):
page, alice_user = alice
title = f"Profile Tab Test {int(time.time())}"
_create_gist(page, title=title, source_code="profile_tab = True")
page.goto(
f"{BASE_URL}/profile/{alice_user['username']}?tab=gists",
wait_until="domcontentloaded",
)
assert page.is_visible(f"text={title}")
def test_profile_rank_stat(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
assert page.is_visible("text=Rank")
def test_ui_media_tab_link_visible(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
assert page.locator(".profile-tab:has-text('Media')").is_visible()
def test_ui_media_tab_shows_thumbnail(alice):
page, user = alice
_seed_media_via_browser(page)
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=media",
wait_until="domcontentloaded",
)
assert page.locator(".profile-tab.active:has-text('Media')").is_visible()
thumb = page.locator(".media-grid img.gallery-thumb[data-lightbox]").first
thumb.wait_for(state="visible")
assert thumb.get_attribute("data-full")
def test_ui_media_lightbox_opens(alice):
page, user = alice
_seed_media_via_browser(page)
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=media",
wait_until="domcontentloaded",
)
thumb = page.locator(".media-grid img.gallery-thumb[data-lightbox]").first
thumb.wait_for(state="visible")
thumb.click()
overlay = page.locator(".lightbox-overlay.visible")
overlay.wait_for(state="visible")
expect(page.locator(".lightbox-overlay.visible .lightbox-image")).to_be_visible()
page.locator(".lightbox-overlay.visible .lightbox-close").click()
overlay.wait_for(state="hidden")
def test_ui_owner_delete_removes_tile(alice):
page, user = alice
uid = _seed_media_via_browser(page, title="to delete")
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=media",
wait_until="domcontentloaded",
)
tile = page.locator(f".media-tile[data-media-tile='{uid}']")
tile.wait_for(state="attached")
page.locator(f".media-tile[data-media-tile='{uid}'] .media-delete-btn").click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
tile.wait_for(state="detached")
assert page.locator(f".media-tile[data-media-tile='{uid}']").count() == 0
def test_ui_member_sees_no_delete_button_on_other_profile(bob, alice):
# alice (admin) seeds media on her own profile
page_a, user_a = alice
_seed_media_via_browser(page_a)
# bob (member) views alice's gallery: no delete controls
page_b, _ = bob
page_b.goto(
f"{BASE_URL}/profile/{user_a['username']}?tab=media",
wait_until="domcontentloaded",
)
page_b.locator(".media-grid img.gallery-thumb").first.wait_for(state="visible")
assert page_b.locator(".media-delete-btn").count() == 0
def test_ui_mobile_media_grid(mobile_page):
page, user = mobile_page
_seed_media_via_browser(page)
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=media",
wait_until="domcontentloaded",
)
grid = page.locator(".media-grid")
grid.wait_for(state="visible")
assert page.locator(".media-grid .media-tile").count() >= 1
def test_profile_tab_renders_for_owner(bob):
page, user = bob
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=notifications",
wait_until="domcontentloaded",
)
expect(page.locator("[data-notif-prefs]")).to_be_visible()
expect(
page.locator("[data-notif-toggle][data-type='vote'][data-channel='push']")
).to_have_count(1)
assert page.is_visible("text=Direct messages")
def test_owner_toggle_via_http_persists(bob):
_, user = bob
target = _user_notification_prefs(user["username"])
reset_notification_prefs(target["uid"])
try:
response = requests.post(
f"{BASE_URL}/profile/{target['username']}/notifications",
data={"notification_type": "mention", "channel": "push", "value": "false"},
headers={"Accept": "application/json", "X-API-KEY": target["api_key"]},
allow_redirects=False,
)
assert response.status_code == 200
assert response.json()["ok"] is True
assert notification_enabled(target["uid"], "mention", "push") is False
assert notification_enabled(target["uid"], "mention", "in_app") is True
finally:
reset_notification_prefs(target["uid"])
def test_non_owner_non_admin_forbidden(bob):
_, user = bob
member = _user_notification_prefs(user["username"])
target = _user_notification_prefs("alice_test")
response = requests.post(
f"{BASE_URL}/profile/{target['username']}/notifications",
data={"notification_type": "vote", "channel": "in_app", "value": "false"},
headers={"Accept": "application/json", "X-API-KEY": member["api_key"]},
allow_redirects=False,
)
assert response.status_code == 403
def test_admin_edits_other_user(alice):
_, admin_user = alice
admin = _user_notification_prefs(admin_user["username"])
target = _user_notification_prefs("bob_test")
reset_notification_prefs(target["uid"])
try:
response = requests.post(
f"{BASE_URL}/profile/{target['username']}/notifications",
data={"notification_type": "comment", "channel": "push", "value": "false"},
headers={"Accept": "application/json", "X-API-KEY": admin["api_key"]},
allow_redirects=False,
)
assert response.status_code == 200
assert notification_enabled(target["uid"], "comment", "push") is False
finally:
reset_notification_prefs(target["uid"])
def test_profile_search_returns_results(alice):
page, _ = alice
resp = page.request.get(f"{BASE_URL}/profile/search?q=bob")
assert resp.status == 200
data = resp.json()
assert any("bob" in u["username"] for u in data["results"])
def test_profile_page_loads(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
assert page.is_visible(f"text={user['username']}")
def test_profile_stats(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
assert page.is_visible("text=Posts")
assert page.is_visible("text=Level")
assert page.is_visible("text=Stars")
def test_profile_level_bar(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
assert page.is_visible("text=Progress to next level")
level_bar = page.locator(".profile-level-bar .bar")
assert level_bar.is_visible()
def test_profile_badges(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
badges = page.locator(".profile-badge")
assert badges.count() >= 1
def test_profile_tabs(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
tabs = ["Posts", "Projects", "Activity"]
for tab in tabs:
assert page.is_visible(f"a:has-text('{tab}')")
def test_profile_posts_tab(alice):
page, user = alice
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=posts",
wait_until="domcontentloaded",
)
assert page.locator(".profile-tab.active:has-text('Posts')").is_visible()
assert (
page.locator(".profile-posts .post-card").count() > 0
or page.locator(".profile-posts .empty-state:has-text('No posts yet.')").count()
> 0
)
def test_profile_projects_tab_content(alice):
page, user = alice
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=projects",
wait_until="domcontentloaded",
)
assert page.locator(".profile-tab.active:has-text('Projects')").is_visible()
assert (
page.locator(".profile-posts .project-card").count() > 0
or page.locator(
".profile-posts .empty-state:has-text('No projects yet.')"
).count()
> 0
)
def test_profile_info_section(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
assert page.is_visible("text=Bio")
assert page.is_visible("text=Location")
def test_profile_back_link(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
back = page.locator("a:has-text('Back')").first
assert back.is_visible()
back.click()
page.wait_for_url(f"{BASE_URL}/feed", wait_until="domcontentloaded")
def test_profile_level_display(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
assert page.locator(".profile-level-bar").is_visible()
def test_profile_avatar(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
avatar = page.locator(".avatar-lg")
assert avatar.is_visible()
def test_profile_edit_bio(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
page.evaluate("""
document.querySelector('textarea[name="bio"]').value = 'Test bio for testing';
document.querySelector('form[action="/profile/update"]').submit();
""")
page.wait_for_url(
f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
)
page.locator("text=Test bio for testing").first.wait_for(state="visible")
def test_profile_edit_location(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
page.evaluate("""
document.querySelector('input[name="location"]').value = 'Amsterdam';
document.querySelector('form[action="/profile/update"]').submit();
""")
page.wait_for_url(
f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
)
page.locator("text=Amsterdam").first.wait_for(state="visible")
def test_profile_edit_all_fields(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
page.evaluate("""
document.querySelector('textarea[name="bio"]').value = 'Full stack dev';
document.querySelector('input[name="location"]').value = 'London, UK';
document.querySelector('input[name="git_link"]').value = 'https://git.example.com/alice';
document.querySelector('input[name="website"]').value = 'https://alice.example.com';
document.querySelector('form[action="/profile/update"]').submit();
""")
page.wait_for_url(
f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
)
page.locator("text=Full stack dev").first.wait_for(state="visible")
assert page.is_visible("text=London, UK")
assert page.is_visible("text=https://git.example.com/alice")
assert page.is_visible("text=https://alice.example.com")
def test_profile_message_button_self(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
message_btn = page.locator("a:has-text('Send Message')")
assert not message_btn.is_visible()
def test_profile_activity_tab(alice):
page, user = alice
page.goto(
f"{BASE_URL}/profile/{user['username']}?tab=activity",
wait_until="domcontentloaded",
)
assert page.locator(".profile-tab.active:has-text('Activity')").is_visible()
assert (
page.locator(".profile-posts .activity-card").count() > 0
or page.locator(
".profile-posts .empty-state:has-text('No activity yet.')"
).count()
> 0
)
def test_profile_followers_json(alice):
page, user = alice
resp = page.request.get(f"{BASE_URL}/profile/{user['username']}/followers")
assert resp.status == 200
data = resp.json()
assert data["username"] == user["username"]
assert data["mode"] == "followers"
assert "followers" in data
def test_profile_following_json(alice):
page, user = alice
resp = page.request.get(f"{BASE_URL}/profile/{user['username']}/following")
assert resp.status == 200
data = resp.json()
assert data["username"] == user["username"]
assert data["mode"] == "following"
assert "following" in data
def test_profile_customization_global_toggle(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
form = page.locator(f"form[action='/profile/{user['username']}/customization/global']")
if form.count() > 0:
button = form.locator("button[type='submit']")
expect(button).to_have_text("Disable")
button.click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
expect(button).to_have_text("Enable")
button.click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
expect(button).to_have_text("Disable")
def test_profile_customization_pagetype_toggle(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
form = page.locator(f"form[action='/profile/{user['username']}/customization/pagetype']")
if form.count() > 0:
button = form.locator("button[type='submit']")
expect(button).to_have_text("Disable")
button.click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
expect(button).to_have_text("Enable")
button.click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
expect(button).to_have_text("Disable")
def test_profile_page_has_structured_data(alice):
page, user = alice
page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded")
scripts = page.locator('script[type="application/ld+json"]')
count = scripts.count()
assert count >= 1
text = scripts.first.text_content()
assert "ProfilePage" in text or "WebSite" in text