forked from retoor/devplacepy
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.
This commit is contained in:
+49
-102
@@ -1,120 +1,67 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import re
|
||||
from tests.conftest import BASE_URL
|
||||
FOLLOW = "form[action='/follow/bob_test'] button"
|
||||
UNFOLLOW = "form[action='/follow/unfollow/bob_test'] button"
|
||||
from playwright.sync_api import expect
|
||||
from tests.e2e.post import create_post
|
||||
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
CONSENT = "ai_third_party"
|
||||
|
||||
|
||||
def test_profile_shows_online_presence(alice):
|
||||
page, user = alice
|
||||
def _privacy_tab(page, username):
|
||||
page.goto(
|
||||
f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded"
|
||||
f"{BASE_URL}/profile/{username}?tab=privacy", wait_until="domcontentloaded"
|
||||
)
|
||||
presence = page.locator(".profile-presence")
|
||||
expect(presence).to_be_visible()
|
||||
expect(presence).to_have_class(re.compile(r"\bonline\b"))
|
||||
expect(presence).to_have_text("online")
|
||||
page.locator(".privacy-panel").wait_for(state="visible")
|
||||
|
||||
|
||||
def test_nav_avatar_shows_online_presence_dot(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
nav_dot = page.locator(".topnav-user .presence-dot")
|
||||
expect(nav_dot).to_be_visible()
|
||||
expect(nav_dot).to_have_class(re.compile(r"\bonline\b"))
|
||||
def test_the_owner_toggles_a_consent_from_the_privacy_tab(bob):
|
||||
page, user = bob
|
||||
_privacy_tab(page, user["username"])
|
||||
row = page.locator(f".privacy-panel tr:has(input[value='{CONSENT}'])")
|
||||
button = row.locator("button[type='submit']")
|
||||
original = (button.inner_text() or "").strip()
|
||||
flipped = "Withdraw" if original == "Grant" else "Grant"
|
||||
try:
|
||||
button.click()
|
||||
expect(row.locator("button[type='submit']")).to_have_text(
|
||||
flipped, timeout=10000
|
||||
)
|
||||
finally:
|
||||
_privacy_tab(page, user["username"])
|
||||
if (row.locator("button[type='submit']").inner_text() or "").strip() != original:
|
||||
row.locator("button[type='submit']").click()
|
||||
expect(row.locator("button[type='submit']")).to_have_text(
|
||||
original, timeout=10000
|
||||
)
|
||||
|
||||
|
||||
def _post_comment(page, body):
|
||||
textarea = page.locator(".comment-form textarea[name='content']")
|
||||
textarea.fill(body)
|
||||
page.locator(".comment-form button:has-text('Post')").click()
|
||||
expect(page.locator(f".comment-text:has-text('{body}')")).to_be_visible()
|
||||
|
||||
|
||||
def test_activity_comment_card_links_to_parent_post(alice):
|
||||
page, user = alice
|
||||
create_post(page, "devlog", "Post for activity comment link")
|
||||
post_url = page.url
|
||||
_post_comment(page, "Activity clickable comment body")
|
||||
|
||||
page.goto(
|
||||
f"{BASE_URL}/profile/{user['username']}?tab=activity",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
card_link = page.locator(
|
||||
".activity-card.card-link-host a.card-link[href*='#comment-']"
|
||||
).first
|
||||
expect(card_link).to_have_count(1)
|
||||
href = card_link.get_attribute("href")
|
||||
assert "/posts/" in href and "#comment-" in href
|
||||
|
||||
card_link.click()
|
||||
page.wait_for_url("**/posts/**", wait_until="domcontentloaded")
|
||||
def test_the_owner_sees_every_privacy_control(bob):
|
||||
page, user = bob
|
||||
_privacy_tab(page, user["username"])
|
||||
panel = page.locator(".privacy-panel")
|
||||
expect(panel.locator(f"form[action='/profile/{user['username']}/consent']").first).to_be_attached()
|
||||
expect(
|
||||
page.locator(".comment-text:has-text('Activity clickable comment body')")
|
||||
panel.locator(f"form[action='/profile/{user['username']}/mature-content']")
|
||||
).to_be_visible()
|
||||
expect(panel.locator("a:has-text('Delete account')")).to_be_visible()
|
||||
|
||||
|
||||
def test_activity_post_card_links_to_post(alice):
|
||||
page, user = alice
|
||||
create_post(page, "showcase", "Post for activity post link", title="Activity Post Title")
|
||||
def test_an_admin_reads_the_state_but_gets_no_privacy_controls(alice, seeded_db):
|
||||
admin_page, _ = alice
|
||||
member = seeded_db["bob"]
|
||||
_privacy_tab(admin_page, member["username"])
|
||||
panel = admin_page.locator(".privacy-panel")
|
||||
expect(panel).to_contain_text("Only the account holder can change this.")
|
||||
expect(panel).to_contain_text("Only the account holder can change this preference.")
|
||||
expect(panel.locator("form[action$='/consent']")).to_have_count(0)
|
||||
expect(panel.locator("form[action$='/mature-content']")).to_have_count(0)
|
||||
expect(panel.locator("a:has-text('Delete account')")).to_have_count(0)
|
||||
|
||||
|
||||
def test_a_stranger_never_reaches_the_privacy_panel(bob):
|
||||
page, _ = bob
|
||||
page.goto(
|
||||
f"{BASE_URL}/profile/{user['username']}?tab=activity",
|
||||
wait_until="domcontentloaded",
|
||||
f"{BASE_URL}/profile/alice_test?tab=privacy", wait_until="domcontentloaded"
|
||||
)
|
||||
post_link = page.locator(
|
||||
".activity-card.card-link-host a.card-link[href^='/posts/']:not([href*='#comment-'])"
|
||||
).first
|
||||
expect(post_link).to_have_count(1)
|
||||
post_link.click()
|
||||
page.wait_for_url("**/posts/**", wait_until="domcontentloaded")
|
||||
expect(page.locator(".post-detail-title:has-text('Activity Post Title')")).to_be_visible()
|
||||
|
||||
|
||||
def test_activity_inner_content_link_stays_clickable(alice, bob):
|
||||
page, user = alice
|
||||
create_post(page, "random", "Post for activity mention comment")
|
||||
_post_comment(page, "Ping @bob_test in activity")
|
||||
|
||||
page.goto(
|
||||
f"{BASE_URL}/profile/{user['username']}?tab=activity",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
mention = page.locator(
|
||||
".activity-card.card-link-host .activity-content a[href='/profile/bob_test']"
|
||||
).first
|
||||
expect(mention).to_have_count(1)
|
||||
mention.click()
|
||||
page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def test_follow_then_unfollow(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/profile/bob_test", wait_until="domcontentloaded")
|
||||
if page.is_visible(UNFOLLOW):
|
||||
page.click(UNFOLLOW)
|
||||
page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
|
||||
assert page.is_visible(FOLLOW)
|
||||
page.click(FOLLOW)
|
||||
page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
|
||||
assert page.is_visible(UNFOLLOW)
|
||||
page.click(UNFOLLOW)
|
||||
page.wait_for_url("**/profile/bob_test", wait_until="domcontentloaded")
|
||||
assert page.is_visible(FOLLOW)
|
||||
|
||||
|
||||
def test_profile_viewing_other_user(bob, alice):
|
||||
page_b, user_b = bob
|
||||
page_b.goto(f"{BASE_URL}/profile/alice_test", wait_until="domcontentloaded")
|
||||
assert page_b.is_visible("text=alice_test")
|
||||
|
||||
|
||||
def test_profile_message_button(bob, alice):
|
||||
page_b, _ = bob
|
||||
page_b.goto(f"{BASE_URL}/profile/alice_test", wait_until="domcontentloaded")
|
||||
message_btn = page_b.locator("a:has-text('Send Message')")
|
||||
assert message_btn.is_visible()
|
||||
expect(page.locator(".privacy-panel")).to_have_count(0)
|
||||
expect(page.locator("form[action='/profile/alice_test/consent']")).to_have_count(0)
|
||||
|
||||
Reference in New Issue
Block a user