824 lines
30 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
from datetime import datetime, timedelta, timezone
from tests.conftest import BASE_URL
from devplacepy.database import get_table
from devplacepy.utils import generate_uid
import requests
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_notifications_pagination(alice):
page, _ = alice
alice_row = get_table("users").find_one(username="alice_test")
notifications_table = get_table("notifications")
notifications_table.delete(user_uid=alice_row["uid"])
now = datetime.now(timezone.utc)
for i in range(30):
notifications_table.insert(
{
"uid": generate_uid(),
"user_uid": alice_row["uid"],
"type": "test",
"message": f"pagination-test-msg-{i:02d}",
"related_uid": alice_row["uid"],
"target_url": "/feed",
"read": False,
"created_at": (now - timedelta(seconds=i)).isoformat(),
}
)
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
cards = page.locator(".notification-card")
assert cards.count() == 25, f"expected 25 cards on page 1, got {cards.count()}"
load_more = page.locator(".load-more-wrap a")
load_more.wait_for(state="visible", timeout=5000)
href = load_more.get_attribute("href")
assert href and "before=" in href, f"Load More should carry cursor: {href}"
page.goto(f"{BASE_URL}{href}", wait_until="domcontentloaded")
cards2 = page.locator(".notification-card")
assert cards2.count() == 5, f"expected 5 cards on page 2, got {cards2.count()}"
assert page.locator(".load-more-wrap").count() == 0, (
"Load More should be absent on final page"
)
notifications_table.delete(user_uid=alice_row["uid"])
def test_notifications_no_load_more_when_under_page_size(alice):
page, _ = alice
alice_row = get_table("users").find_one(username="alice_test")
notifications_table = get_table("notifications")
notifications_table.delete(user_uid=alice_row["uid"])
now = datetime.now(timezone.utc)
for i in range(3):
notifications_table.insert(
{
"uid": generate_uid(),
"user_uid": alice_row["uid"],
"type": "test",
"message": f"small-set-msg-{i}",
"related_uid": alice_row["uid"],
"target_url": "/feed",
"read": False,
"created_at": (now - timedelta(seconds=i)).isoformat(),
}
)
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
assert page.locator(".notification-card").count() == 3
assert page.locator(".load-more-wrap").count() == 0
notifications_table.delete(user_uid=alice_row["uid"])
def test_notifications_page_loads(alice):
page, _ = alice
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
assert page.is_visible("h2:has-text('Notifications')")
def test_mark_all_read(alice):
page, _ = alice
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
clear = page.locator("form[action='/notifications/mark-all-read'] button")
if clear.is_visible():
clear.click()
page.wait_for_timeout(500)
def test_notifications_navigation(alice):
page, _ = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
assert page.is_visible("h2:has-text('Notifications')")
def test_notifications_empty_state(alice):
page, _ = alice
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
assert page.is_visible("h2:has-text('Notifications')") or page.is_visible(
"text=No notifications yet"
)
def test_notifications_header_has_clear(alice):
page, _ = alice
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
clear = page.locator("form[action='/notifications/mark-all-read'] button")
assert clear.is_visible()
def test_notifications_topnav_user(alice):
page, user = alice
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
assert page.is_visible(f"text={user['username']}")
def test_notifications_avatar_visible(alice):
page, _ = alice
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
avatar = page.locator("img.avatar-img").first
assert avatar.is_visible()
def test_vote_notification_on_post(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pb.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pb.locator(".feed-fab").first.click()
pb.fill("#post-content", "Post for vote notification test")
pb.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pb.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
post_url = pb.url
pa.goto(post_url, wait_until="domcontentloaded")
pa.wait_for_timeout(1000)
vote_btn = pa.locator("button.post-action-btn").filter(has_text="+").first
vote_btn.wait_for(state="visible", timeout=10000)
vote_btn.click()
pa.wait_for_timeout(1500)
pa.wait_for_timeout(500)
pa_body = pa.locator("body").text_content()
assert "Internal Server Error" not in pa_body, (
f"Alice got 500 after vote: {pa_body[:300]}"
)
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pb.wait_for_timeout(1500)
body = pb.locator("body").text_content()
assert "Internal Server Error" not in body, (
f"Got 500 error on notifications page: {body[:500]}"
)
assert "alice_test" in body, (
f"Expected 'alice_test' in notifications, got: {body[:500]}"
)
assert "++'d" in body, f"Expected '++\\'d' in notifications, got: {body[:500]}"
ctx_a.close()
ctx_b.close()
def test_comment_notification_on_post(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pa.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pa.locator(".feed-fab").first.click()
pa.fill("#post-content", "Post for comment notification test")
pa.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pa.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
post_url = pa.url
pb.goto(post_url, wait_until="domcontentloaded")
pb.wait_for_timeout(1000)
comment_textarea = pb.locator("form.comment-form textarea[name='content']").first
comment_textarea.wait_for(state="visible", timeout=10000)
comment_textarea.fill("Nice post!")
pb.locator("button.comment-form-submit").first.click()
pb.wait_for_timeout(1500)
pb_body = pb.locator("body").text_content()
assert "Internal Server Error" not in pb_body, f"Bob got 500: {pb_body[:300]}"
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pa.wait_for_timeout(1500)
body = pa.locator("body").text_content()
assert "Internal Server Error" not in body, (
f"Got 500 error on notifications: {body[:500]}"
)
assert "bob_test" in body, (
f"Expected 'bob_test' in notifications, got: {body[:500]}"
)
assert "commented on your" in body, (
f"Expected 'commented on your' in notifications, got: {body[:500]}"
)
ctx_a.close()
ctx_b.close()
def test_follow_notification(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/profile/alice_test", wait_until="domcontentloaded")
pb.wait_for_timeout(1000)
follow_btn = pb.locator("form[action='/follow/alice_test'] button")
if follow_btn.is_visible():
follow_btn.click()
pb.wait_for_timeout(1000)
pb_body = pb.locator("body").text_content()
assert "Internal Server Error" not in pb_body, f"Bob got 500: {pb_body[:300]}"
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pa.wait_for_timeout(1500)
body = pa.locator("body").text_content()
assert "Internal Server Error" not in body, (
f"Got 500 error on notifications: {body[:500]}"
)
assert "bob_test" in body, (
f"Expected 'bob_test' in notifications, got: {body[:500]}"
)
assert "started following you" in body, (
f"Expected 'started following you' in notifications, got: {body[:500]}"
)
ctx_a.close()
ctx_b.close()
def test_message_notification(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/messages?search=alice_test", wait_until="domcontentloaded")
pb.wait_for_timeout(2000)
2026-07-23 00:02:43 +02:00
msg_input = pb.locator("textarea[name='content']").first
msg_input.wait_for(state="visible", timeout=10000)
msg_input.fill("Hello from bob_test!")
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
pb.locator(".messages-input-area button[type='submit']").last.click()
pb.wait_for_timeout(1500)
pb_body = pb.locator("body").text_content()
assert "Internal Server Error" not in pb_body, f"Bob got 500: {pb_body[:300]}"
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pa.wait_for_timeout(1500)
body = pa.locator("body").text_content()
assert "Internal Server Error" not in body, (
f"Got 500 error on notifications: {body[:500]}"
)
assert "bob_test" in body, (
f"Expected 'bob_test' in notifications, got: {body[:500]}"
)
assert "sent you a message" in body, (
f"Expected 'sent you a message' in notifications, got: {body[:500]}"
)
ctx_a.close()
ctx_b.close()
def test_mention_notification_in_post(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pb.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pb.locator(".feed-fab").first.click()
pb.fill("#post-content", "Check this out @alice_test how are you?")
pb.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pb.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pa.wait_for_timeout(2000)
body = pa.locator("body").text_content()
assert "Internal Server Error" not in body, (
f"Got 500 error on notifications: {body[:500]}"
)
assert "bob_test" in body, (
f"Expected 'bob_test' in notifications for mention, got: {body[:500]}"
)
assert "mentioned you" in body, (
f"Expected 'mentioned you' in notifications, got: {body[:500]}"
)
ctx_a.close()
ctx_b.close()
def test_mention_notification_in_comment(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pa.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pa.locator(".feed-fab").first.click()
pa.fill("#post-content", "Post for mention in comment test")
pa.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pa.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
post_url = pa.url
pb.goto(post_url, wait_until="domcontentloaded")
pb.wait_for_timeout(1000)
comment_textarea = pb.locator("form.comment-form textarea[name='content']").first
comment_textarea.wait_for(state="visible", timeout=10000)
comment_textarea.fill("Hey @alice_test look at this!")
pb.locator("button.comment-form-submit").first.click()
pb.wait_for_timeout(1500)
pb_body = pb.locator("body").text_content()
assert "Internal Server Error" not in pb_body, f"Bob got 500: {pb_body[:300]}"
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pa.wait_for_timeout(2000)
body = pa.locator("body").text_content()
assert "Internal Server Error" not in body, (
f"Got 500 error on notifications: {body[:500]}"
)
assert "bob_test" in body, (
f"Expected 'bob_test' in notifications for mention, got: {body[:500]}"
)
assert "mentioned you" in body, (
f"Expected 'mentioned you' in notifications, got: {body[:500]}"
)
ctx_a.close()
ctx_b.close()
def test_reply_notification(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pa.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pa.locator(".feed-fab").first.click()
pa.fill("#post-content", "Post for reply notification test")
pa.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pa.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
post_url = pa.url
pa.goto(post_url, wait_until="domcontentloaded")
pa.wait_for_timeout(1000)
comment_textarea = pa.locator("form.comment-form textarea[name='content']").first
comment_textarea.wait_for(state="visible", timeout=10000)
comment_textarea.fill("Alice's top-level comment")
pa.locator("button.comment-form-submit").first.click()
pa.wait_for_timeout(1500)
pb.goto(post_url, wait_until="domcontentloaded")
pb.wait_for_timeout(1000)
reply_btn = pb.locator("button:has-text('Reply')").first
if reply_btn.is_visible():
reply_btn.click()
pb.wait_for_timeout(500)
reply_textarea = pb.locator("form.comment-form textarea[name='content']").first
if reply_textarea.is_visible():
reply_textarea.fill("Bob's reply to Alice's comment")
pb.locator("button.comment-form-submit").first.click()
pb.wait_for_timeout(1500)
pb_body = pb.locator("body").text_content()
assert "Internal Server Error" not in pb_body, f"Bob got 500: {pb_body[:300]}"
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pa.wait_for_timeout(2000)
body = pa.locator("body").text_content()
assert "Internal Server Error" not in body, (
f"Got 500 error on notifications: {body[:500]}"
)
assert "bob_test" in body, (
f"Expected 'bob_test' in notifications, got: {body[:500]}"
)
assert "replied to your comment" in body, (
f"Expected 'replied to your comment' in notifications, got: {body[:500]}"
)
ctx_a.close()
ctx_b.close()
def test_mention_notification_names_actor(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pb.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pb.locator(".feed-fab").first.click()
pb.fill("#post-content", "Naming check @alice_test please read")
pb.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pb.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pa.wait_for_timeout(1500)
texts = pa.locator(".notification-text").all_text_contents()
assert any("@bob_test mentioned you" in t for t in texts), (
f"mention message must name the actor (bob): {texts}"
)
assert not any("@alice_test mentioned you" in t for t in texts), (
f"mention message must not name the mentioned user (alice): {texts}"
)
ctx_a.close()
ctx_b.close()
def test_comment_notification_click_opens_comment(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pa.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pa.locator(".feed-fab").first.click()
pa.fill("#post-content", "Post for click navigation test")
pa.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pa.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
post_url = pa.url
pb.goto(post_url, wait_until="domcontentloaded")
textarea = pb.locator("form.comment-form textarea[name='content']").first
textarea.wait_for(state="visible", timeout=10000)
textarea.fill("Bob comment that alice should jump to")
pb.locator("button.comment-form-submit").first.click()
pb.wait_for_timeout(1500)
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
card = (
pa.locator(".notification-card").filter(has_text="commented on your post").first
)
card.wait_for(state="visible", timeout=10000)
href = card.locator("a.card-link").get_attribute("href")
assert href.startswith("/notifications/open/"), f"unexpected href: {href}"
card.locator("a.card-link").click()
pa.wait_for_url("**/posts/**", timeout=10000, wait_until="domcontentloaded")
assert "#comment-" in pa.url, f"click did not deep-link to a comment: {pa.url}"
comment_uid = pa.url.split("#comment-")[1]
pa.locator(f"#comment-{comment_uid}").wait_for(state="visible", timeout=10000)
pa.wait_for_selector(f"#comment-{comment_uid}.comment-highlight", timeout=5000)
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
pa.wait_for_timeout(1000)
target = pa.locator(f'.notification-card:has(a.card-link[href="{href}"])')
target.wait_for(state="visible", timeout=10000)
assert "unread" not in (target.get_attribute("class") or ""), (
"opening a notification should mark it read"
)
ctx_a.close()
ctx_b.close()
def test_follow_notification_click_opens_profile(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/profile/alice_test", wait_until="domcontentloaded")
pb.wait_for_timeout(1000)
follow_btn = pb.locator("form[action='/follow/alice_test'] button")
if follow_btn.is_visible():
follow_btn.click()
pb.wait_for_timeout(1000)
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
card = (
pa.locator(".notification-card").filter(has_text="started following you").first
)
card.wait_for(state="visible", timeout=10000)
card.locator("a.card-link").click()
pa.wait_for_url("**/profile/bob_test", timeout=10000, wait_until="domcontentloaded")
assert pa.url.endswith("/profile/bob_test"), (
f"follow notification should open the follower profile: {pa.url}"
)
ctx_a.close()
ctx_b.close()
def test_message_notification_click_opens_conversation(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/messages?search=alice_test", wait_until="domcontentloaded")
pb.wait_for_timeout(2000)
2026-07-23 00:02:43 +02:00
msg_input = pb.locator("textarea[name='content']").first
msg_input.wait_for(state="visible", timeout=10000)
msg_input.fill("Click-through message from bob")
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
pb.locator(".messages-input-area button[type='submit']").last.click()
pb.wait_for_timeout(1500)
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
card = pa.locator(".notification-card").filter(has_text="sent you a message").first
card.wait_for(state="visible", timeout=10000)
card.locator("a.card-link").click()
pa.wait_for_url("**/messages**", timeout=10000, wait_until="domcontentloaded")
assert "with_uid=" in pa.url, (
f"message notification should open the conversation: {pa.url}"
)
ctx_a.close()
ctx_b.close()
def test_vote_notification_click_opens_target(app_server, browser, seeded_db):
from tests.conftest import login_user
ctx_a = browser.new_context(viewport={"width": 1400, "height": 900})
ctx_b = browser.new_context(viewport={"width": 1400, "height": 900})
pa = ctx_a.new_page()
pb = ctx_b.new_page()
pa.set_default_timeout(15000)
pb.set_default_timeout(15000)
login_user(pa, seeded_db["alice"])
login_user(pb, seeded_db["bob"])
pb.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
pb.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
pb.locator(".feed-fab").first.click()
pb.fill("#post-content", "Post for vote click-through test")
pb.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
pb.wait_for_url("**/posts/*", timeout=10000, wait_until="domcontentloaded")
post_path = "/posts/" + pb.url.split("/posts/")[1]
pa.goto(f"{BASE_URL}{post_path}", wait_until="domcontentloaded")
pa.wait_for_timeout(1000)
vote_btn = pa.locator("button.post-action-btn").filter(has_text="+").first
vote_btn.wait_for(state="visible", timeout=10000)
vote_btn.click()
pa.wait_for_timeout(1500)
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
card = pb.locator(".notification-card").filter(has_text="++'d").first
card.wait_for(state="visible", timeout=10000)
card.locator("a.card-link").click()
pb.wait_for_url(f"**{post_path}", timeout=10000, wait_until="domcontentloaded")
assert post_path in pb.url, (
f"vote notification should open the voted post: {pb.url}"
)
ctx_a.close()
ctx_b.close()
def test_notifications_page_is_noindex(alice):
page, user = alice
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
page.wait_for_url("**/notifications", wait_until="domcontentloaded")
meta = page.locator('meta[name="robots"]')
content = meta.get_attribute("content")
assert content and "noindex" in content