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:
@@ -40,6 +40,7 @@ def _make_user_devii_quota(role="Member"):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": username,
|
||||
"terms_version": "1",
|
||||
"email": f"{username}@t.dev",
|
||||
"api_key": generate_uid(),
|
||||
"role": role,
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -111,6 +113,8 @@ def _member():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -27,6 +27,8 @@ def _make_admin():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": password,
|
||||
"confirm_password": password,
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -25,6 +25,8 @@ def _make_admin():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": password,
|
||||
"confirm_password": password,
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -26,6 +26,7 @@ def _make_user_ai_usage_profile(role="Member"):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": username,
|
||||
"terms_version": "1",
|
||||
"email": f"{username}@t.dev",
|
||||
"api_key": api_key,
|
||||
"role": role,
|
||||
|
||||
@@ -42,6 +42,7 @@ def seed_extra_users(count=30):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"pagu_{i:04d}",
|
||||
"terms_version": "1",
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -55,6 +56,7 @@ def _seed_target_user():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"target_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"target_{uid[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -111,6 +113,8 @@ def _member():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -194,6 +198,8 @@ def _signup_media(prefix="media"):
|
||||
"email": f"{name}@test.devplace",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -296,6 +302,8 @@ def test_admin_users_pagination(alice):
|
||||
"email": f"pagu{i:04d}@test.devplace",
|
||||
"password": "testpass123",
|
||||
"confirm_password": "testpass123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
}
|
||||
).encode()
|
||||
try:
|
||||
|
||||
@@ -26,6 +26,7 @@ def _make_user_ai_usage_profile(role="Member"):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": username,
|
||||
"terms_version": "1",
|
||||
"email": f"{username}@t.dev",
|
||||
"api_key": api_key,
|
||||
"role": role,
|
||||
|
||||
@@ -46,6 +46,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -15,6 +15,8 @@ def test_login_success(page, app_server):
|
||||
page.fill("#email", "login@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
@@ -34,6 +36,8 @@ def test_login_wrong_password(page, app_server):
|
||||
page.fill("#email", "wrongpw@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
@@ -52,6 +56,8 @@ def test_login_remember_me(page, app_server):
|
||||
page.fill("#email", "remember@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
@@ -72,6 +78,8 @@ def test_logout(page, app_server):
|
||||
page.fill("#email", "logout@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ def test_reset_password_full_flow(page, app_server):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": uname,
|
||||
"terms_version": "1",
|
||||
"email": email,
|
||||
"password_hash": hash_password("oldpass123"),
|
||||
"bio": "",
|
||||
|
||||
@@ -69,6 +69,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -211,6 +212,8 @@ def test_signup_success(page, app_server):
|
||||
page.fill("#email", "fresh@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=fresh_user")
|
||||
@@ -222,6 +225,8 @@ def test_signup_existing_username(page, app_server):
|
||||
page.fill("#email", "dup1@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
@@ -231,6 +236,8 @@ def test_signup_existing_username(page, app_server):
|
||||
page.fill("#email", "dup2@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
expect(page.locator("text=Username already taken")).to_be_visible()
|
||||
|
||||
@@ -241,6 +248,8 @@ def test_signup_existing_email(page, app_server):
|
||||
page.fill("#email", "sameemail@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
@@ -250,6 +259,8 @@ def test_signup_existing_email(page, app_server):
|
||||
page.fill("#email", "sameemail@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
expect(page.locator("text=Email already registered")).to_be_visible()
|
||||
|
||||
@@ -260,6 +271,8 @@ def test_signup_password_mismatch(page, app_server):
|
||||
page.fill("#email", "mismatch@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "different456")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
expect(page.locator("text=Passwords do not match")).to_be_visible()
|
||||
|
||||
@@ -270,6 +283,8 @@ def test_signup_short_password(page, app_server):
|
||||
page.fill("#email", "shortpw@test.devplace")
|
||||
page.fill("#password", "ab")
|
||||
page.fill("#confirm_password", "ab")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
expect(page.locator("text=Password must be at least 6 characters")).to_be_visible()
|
||||
|
||||
@@ -280,6 +295,8 @@ def test_signup_invalid_username(page, app_server):
|
||||
page.fill("#email", "shortname@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
expect(
|
||||
page.locator("text=Username must be between 3 and 32 characters")
|
||||
@@ -316,6 +333,8 @@ def test_landing_authenticated_shows_dashboard(page, app_server):
|
||||
page.fill("#email", "home@test.devplace")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
with page.expect_navigation(timeout=10000):
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
@@ -346,6 +365,8 @@ def test_registration_closed_blocks_signup(alice):
|
||||
"email": "closed_signup@test.devplace",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
)
|
||||
assert "Registration is currently closed" in attempt.text
|
||||
@@ -379,6 +400,8 @@ def test_session_length_setting_applies(alice, browser):
|
||||
guest.fill("#email", f"{username}@test.devplace")
|
||||
guest.fill("#password", "secret123")
|
||||
guest.fill("#confirm_password", "secret123")
|
||||
guest.fill("#birth_date", "1990-01-01")
|
||||
guest.check("#accept_terms")
|
||||
guest.click("button:has-text('Create account')")
|
||||
guest.wait_for_url("**/feed", wait_until="domcontentloaded")
|
||||
|
||||
@@ -397,6 +420,8 @@ def test_feed_page_has_unique_title(page, app_server):
|
||||
page.fill("#email", "seo_title@test.dev")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
title = page.title()
|
||||
@@ -416,6 +441,8 @@ def test_feed_page_has_canonical(page, app_server):
|
||||
page.fill("#email", "seo_canon@test.dev")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
link = page.locator('link[rel="canonical"]')
|
||||
@@ -429,6 +456,8 @@ def test_feed_page_has_og_tags(page, app_server):
|
||||
page.fill("#email", "seo_og@test.dev")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
og = page.locator('meta[property="og:title"]')
|
||||
@@ -442,6 +471,8 @@ def test_feed_page_has_twitter_card(page, app_server):
|
||||
page.fill("#email", "seo_twit@test.dev")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
card = page.locator('meta[name="twitter:card"]')
|
||||
|
||||
@@ -50,6 +50,8 @@ def _member():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -40,6 +40,7 @@ def _make_user_devii_quota(role="Member"):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": username,
|
||||
"terms_version": "1",
|
||||
"email": f"{username}@t.dev",
|
||||
"api_key": generate_uid(),
|
||||
"role": role,
|
||||
|
||||
@@ -30,6 +30,8 @@ def _author():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -52,6 +52,7 @@ def _seed_posts(count):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"pag_{suite}_{i}",
|
||||
"terms_version": "1",
|
||||
"email": f"{suite}_{i}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -84,6 +85,7 @@ def _seed_post_with_comments(comment_texts, topic="devlog"):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"seed_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@seed.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -148,6 +150,7 @@ def _seed_searchable_posts():
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"srch_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -229,6 +232,8 @@ def _author():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -282,6 +287,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -24,6 +24,8 @@ def _register_session(prefix):
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -34,6 +36,7 @@ def _seed_gists(count):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"pag_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -99,6 +102,7 @@ def _seed_searchable_gists(language="python"):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"gsrch_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -408,6 +412,7 @@ def _seed_gist_with_comments():
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"gcseed_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@gc.seed",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -21,6 +21,8 @@ def _signup_media(prefix="media"):
|
||||
"email": f"{name}@test.devplace",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -101,6 +101,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -322,6 +323,7 @@ def _seed_news_with_comments():
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"ncseed_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@nc.seed",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -41,6 +41,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -443,7 +444,7 @@ def test_message_notification(app_server, browser, seeded_db):
|
||||
msg_input = pb.locator("textarea[name='content']").first
|
||||
msg_input.wait_for(state="visible", timeout=10000)
|
||||
msg_input.fill("Hello from bob_test!")
|
||||
pb.locator("button[type='submit']").last.click()
|
||||
pb.locator(".messages-input-area button[type='submit']").last.click()
|
||||
pb.wait_for_timeout(1500)
|
||||
|
||||
pb_body = pb.locator("body").text_content()
|
||||
@@ -756,7 +757,7 @@ def test_message_notification_click_opens_conversation(app_server, browser, seed
|
||||
msg_input = pb.locator("textarea[name='content']").first
|
||||
msg_input.wait_for(state="visible", timeout=10000)
|
||||
msg_input.fill("Click-through message from bob")
|
||||
pb.locator("button[type='submit']").last.click()
|
||||
pb.locator(".messages-input-area button[type='submit']").last.click()
|
||||
pb.wait_for_timeout(1500)
|
||||
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
|
||||
@@ -83,6 +83,7 @@ def _make_admin_openai_gateway(role="Admin"):
|
||||
{
|
||||
"uid": generate_uid(),
|
||||
"username": username,
|
||||
"terms_version": "1",
|
||||
"email": f"{username}@t.dev",
|
||||
"api_key": api_key,
|
||||
"role": role,
|
||||
@@ -94,6 +95,13 @@ def _config(page, **fields):
|
||||
page.request.post(f"{BASE_URL}/admin/services/openai/config", form=fields)
|
||||
|
||||
|
||||
def _grant_ai_consent(page, username):
|
||||
page.request.post(
|
||||
f"{BASE_URL}/profile/{username}/consent",
|
||||
form={"kind": "ai_third_party", "granted": "1"},
|
||||
)
|
||||
|
||||
|
||||
def test_gateway_disabled_returns_503(alice):
|
||||
page, _ = alice
|
||||
page.request.post(f"{BASE_URL}/admin/services/openai/stop")
|
||||
@@ -130,6 +138,16 @@ def test_gateway_auth_and_routing(alice):
|
||||
assert with_key.status_code == 502 # auth passed, upstream unreachable
|
||||
|
||||
admin_key = get_table("users").find_one(username=user["username"])["api_key"]
|
||||
no_consent = requests.post(
|
||||
f"{BASE_URL}/openai/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {admin_key}"},
|
||||
json={"messages": [{"role": "user", "content": "hi"}]},
|
||||
)
|
||||
assert no_consent.status_code == 403, (
|
||||
"a user's own key must not reach the provider without ai_third_party consent"
|
||||
)
|
||||
|
||||
_grant_ai_consent(page, user["username"])
|
||||
with_admin = requests.post(
|
||||
f"{BASE_URL}/openai/v1/chat/completions",
|
||||
headers={"Authorization": f"Bearer {admin_key}"},
|
||||
|
||||
@@ -9,6 +9,13 @@ def _config(page, **fields):
|
||||
page.request.post(f"{BASE_URL}/admin/services/openai/config", form=fields)
|
||||
|
||||
|
||||
def _grant_ai_consent(page, username):
|
||||
page.request.post(
|
||||
f"{BASE_URL}/profile/{username}/consent",
|
||||
form={"kind": "ai_third_party", "granted": "1"},
|
||||
)
|
||||
|
||||
|
||||
def test_embeddings_disabled_returns_503(alice):
|
||||
page, _ = alice
|
||||
page.request.post(f"{BASE_URL}/admin/services/openai/stop")
|
||||
@@ -53,6 +60,7 @@ def test_embeddings_auth_passes_upstream_unreachable(alice):
|
||||
assert with_key.status_code == 502
|
||||
|
||||
admin_key = get_table("users").find_one(username=user["username"])["api_key"]
|
||||
_grant_ai_consent(page, user["username"])
|
||||
with_admin = requests.post(
|
||||
f"{BASE_URL}/openai/v1/embeddings",
|
||||
headers={"Authorization": f"Bearer {admin_key}"},
|
||||
|
||||
@@ -17,6 +17,8 @@ def _session_comments():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -69,6 +71,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -29,6 +29,7 @@ def _seed_posts(count):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"pag_{suite}_{i}",
|
||||
"terms_version": "1",
|
||||
"email": f"{suite}_{i}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -61,6 +62,7 @@ def _seed_post_with_comments(comment_texts, topic="devlog"):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"seed_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@seed.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -125,6 +127,7 @@ def _seed_searchable_posts():
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"srch_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -203,6 +206,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -518,6 +522,8 @@ def test_post_page_has_structured_data(page, app_server):
|
||||
page.fill("#email", "seo_schema@test.dev")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
page.locator(".feed-fab").click()
|
||||
|
||||
@@ -29,6 +29,7 @@ def _seed_user(prefix):
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"{prefix}_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seed.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
+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)
|
||||
|
||||
@@ -18,6 +18,8 @@ def _signup_api_auth(password="secret123"):
|
||||
"email": email,
|
||||
"password": password,
|
||||
"confirm_password": password,
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -67,6 +69,8 @@ def _register_session(prefix):
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -77,6 +81,7 @@ def _seed_gists(count):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"pag_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -142,6 +147,7 @@ def _seed_searchable_gists(language="python"):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"gsrch_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -189,6 +195,8 @@ def _signup_media(prefix="media"):
|
||||
"email": f"{name}@test.devplace",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -320,6 +328,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -17,6 +17,8 @@ def _signup_project_files():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ def _seed_project():
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"dlowner_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@dl.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
|
||||
@@ -17,6 +17,8 @@ def _signup_project_files():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -133,6 +135,8 @@ def _signup_zip_download():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -19,6 +19,7 @@ def _seed_posts(count):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"pag_{suite}_{i}",
|
||||
"terms_version": "1",
|
||||
"email": f"{suite}_{i}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -51,6 +52,7 @@ def _seed_post_with_comments(comment_texts, topic="devlog"):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"seed_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@seed.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -115,6 +117,7 @@ def _seed_searchable_posts():
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"srch_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -155,6 +158,8 @@ def _signup_project_files():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -256,6 +261,7 @@ def _seed_projects(count):
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"pag_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@test.devplace",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -323,6 +329,8 @@ def _signup_zip_download():
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
@@ -871,6 +879,7 @@ def _seed_project_with_comments():
|
||||
{
|
||||
"uid": owner,
|
||||
"username": f"pcseed_{owner[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{owner[:8]}@pc.seed",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -954,6 +963,8 @@ def _signup_containers_menu(prefix):
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -26,6 +26,8 @@ def _seed_quiz(title="Hub e2e quiz"):
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -38,6 +38,8 @@ def _author_quiz(title, questions=(CHOICE, NUMERIC), **settings):
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from devplacepy.database import get_table, refresh_snapshot
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def _create_post(page, content):
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
page.locator(".feed-fab").first.click()
|
||||
page.fill("#post-content", content)
|
||||
page.locator("#create-post-modal button.btn-primary:has-text('Post')").click()
|
||||
page.wait_for_url(f"{BASE_URL}/posts/*", wait_until="domcontentloaded")
|
||||
return page.url.rsplit("/", 1)[-1]
|
||||
|
||||
|
||||
def test_the_report_control_is_present_on_the_feed_and_a_post(alice, bob):
|
||||
alice_page, _ = alice
|
||||
bob_page, _ = bob
|
||||
_create_post(alice_page, "A post that another member can report from the feed.")
|
||||
|
||||
bob_page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
expect(bob_page.locator(".report-btn").first).to_be_visible()
|
||||
expect(bob_page.locator(".block-btn").first).to_be_attached()
|
||||
|
||||
|
||||
def test_the_report_control_is_absent_on_your_own_content(alice):
|
||||
page, _ = alice
|
||||
slug = _create_post(page, "A post whose author must not see a report control.")
|
||||
page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded")
|
||||
expect(page.locator(".post-detail-actions .report-btn")).to_have_count(0)
|
||||
|
||||
|
||||
def test_reporting_a_post_end_to_end_through_the_dialog(alice, bob):
|
||||
alice_page, _ = alice
|
||||
bob_page, _ = bob
|
||||
slug = _create_post(alice_page, "A post reported end to end through the dialog.")
|
||||
|
||||
bob_page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded")
|
||||
bob_page.locator(".report-btn").first.click()
|
||||
|
||||
dialog = bob_page.locator("#report-dialog")
|
||||
dialog.wait_for(state="visible")
|
||||
bob_page.select_option("#report-reason", "harassment")
|
||||
bob_page.fill("#report-detail", "Reported by the end-to-end test.")
|
||||
bob_page.locator("#report-form button[type='submit']").click()
|
||||
|
||||
expect(bob_page.locator("dp-toast")).to_contain_text("Report received", timeout=8000)
|
||||
|
||||
bob_page.goto(f"{BASE_URL}/reports/mine", wait_until="domcontentloaded")
|
||||
expect(bob_page.locator("table")).to_contain_text("Harassment")
|
||||
|
||||
|
||||
def test_the_admin_queue_shows_the_report_and_its_sla_badge(alice, bob):
|
||||
alice_page, _ = alice
|
||||
bob_page, _ = bob
|
||||
slug = _create_post(alice_page, "A post that must appear in the moderation queue.")
|
||||
|
||||
bob_page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded")
|
||||
bob_page.locator(".report-btn").first.click()
|
||||
bob_page.locator("#report-dialog").wait_for(state="visible")
|
||||
bob_page.select_option("#report-reason", "spam")
|
||||
bob_page.locator("#report-form button[type='submit']").click()
|
||||
expect(bob_page.locator("dp-toast")).to_contain_text("Report received", timeout=8000)
|
||||
|
||||
alice_page.goto(f"{BASE_URL}/admin/moderation", wait_until="domcontentloaded")
|
||||
expect(alice_page.locator(".sla-badge")).to_be_visible()
|
||||
expect(alice_page.locator(".admin-table")).to_contain_text("Spam")
|
||||
|
||||
|
||||
def test_the_report_dialog_lists_every_reason(alice):
|
||||
from devplacepy.database import REPORT_REASONS
|
||||
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
options = page.locator("#report-reason option")
|
||||
assert options.count() == len(REPORT_REASONS)
|
||||
|
||||
|
||||
def test_blocking_from_a_content_action_bar_hides_the_author(alice, bob):
|
||||
alice_page, alice_user = alice
|
||||
bob_page, _ = bob
|
||||
marker = "A post that disappears from the feed after a block."
|
||||
_create_post(alice_page, marker)
|
||||
|
||||
bob_page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
expect(bob_page.locator(".post-card").first).to_be_visible()
|
||||
|
||||
bob_page.locator(".block-btn").first.click()
|
||||
bob_page.locator(".dialog-overlay.visible .dialog-confirm").click()
|
||||
bob_page.wait_for_load_state("domcontentloaded")
|
||||
|
||||
bob_page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
assert marker not in bob_page.content()
|
||||
|
||||
bob_page.goto(
|
||||
f"{BASE_URL}/profile/{alice_user['username']}", wait_until="domcontentloaded"
|
||||
)
|
||||
bob_page.locator("button:has-text('Unblock')").first.click()
|
||||
bob_page.locator(".dialog-overlay.visible .dialog-confirm").click()
|
||||
bob_page.wait_for_load_state("domcontentloaded")
|
||||
|
||||
|
||||
def test_the_footer_links_every_legal_page(page, app_server):
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
footer = page.locator(".footer-links")
|
||||
for label in ("Terms", "Privacy", "Guidelines", "Contact"):
|
||||
expect(footer.locator(f"a:has-text('{label}')")).to_be_visible()
|
||||
|
||||
|
||||
def test_the_legal_pages_render_for_a_guest(page, app_server):
|
||||
for slug in (
|
||||
"terms",
|
||||
"community-guidelines",
|
||||
"privacy",
|
||||
"contact",
|
||||
"content-moderation",
|
||||
"intellectual-property",
|
||||
):
|
||||
page.goto(f"{BASE_URL}/docs/{slug}.html", wait_until="domcontentloaded")
|
||||
expect(page.locator("h1").first).to_be_visible()
|
||||
|
||||
|
||||
def test_the_admin_operations_page_is_hidden_from_a_guest(page, app_server):
|
||||
page.goto(
|
||||
f"{BASE_URL}/docs/moderation-operations.html", wait_until="domcontentloaded"
|
||||
)
|
||||
assert "not found" in page.content().lower()
|
||||
|
||||
|
||||
def test_the_admin_operations_page_renders_for_an_admin(alice):
|
||||
alice_page, _ = alice
|
||||
alice_page.goto(
|
||||
f"{BASE_URL}/docs/moderation-operations.html", wait_until="domcontentloaded"
|
||||
)
|
||||
expect(alice_page.locator("h1").first).to_be_visible()
|
||||
assert "App review checklist" in alice_page.content()
|
||||
|
||||
|
||||
def test_deleting_an_account_through_the_ui_ends_the_login(page, app_server):
|
||||
import time
|
||||
|
||||
name = f"e2edel{int(time.time() * 1000)}"
|
||||
page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded")
|
||||
page.fill("#username", name)
|
||||
page.fill("#email", f"{name}@t.dev")
|
||||
page.fill("#password", "secret123")
|
||||
page.fill("#confirm_password", "secret123")
|
||||
page.fill("#birth_date", "1990-01-01")
|
||||
page.check("#accept_terms")
|
||||
page.click("button:has-text('Create account')")
|
||||
page.wait_for_url("**/feed", wait_until="domcontentloaded")
|
||||
|
||||
page.goto(f"{BASE_URL}/profile/{name}/delete", wait_until="domcontentloaded")
|
||||
page.fill("#delete-password", "secret123")
|
||||
page.locator("button:has-text('Delete my account')").click()
|
||||
page.locator(".dialog-overlay.visible .dialog-confirm").click()
|
||||
page.wait_for_url(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
|
||||
refresh_snapshot()
|
||||
assert get_table("users").find_one(username=name) is None
|
||||
|
||||
page.goto(f"{BASE_URL}/auth/login", wait_until="domcontentloaded")
|
||||
page.fill("#email", f"{name}@t.dev")
|
||||
page.fill("#password", "secret123")
|
||||
page.click("button:has-text('Sign in')")
|
||||
page.wait_for_load_state("domcontentloaded")
|
||||
assert "/feed" not in page.url
|
||||
+2
-1
@@ -41,6 +41,7 @@ def _seed_owner():
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"terms_version": "1",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
@@ -193,7 +194,7 @@ def test_landing_features(page, app_server):
|
||||
features = [
|
||||
"100% Free Forever",
|
||||
"Zero Ads",
|
||||
"No Censorship",
|
||||
"No Gatekeeping",
|
||||
"No Payments",
|
||||
]
|
||||
for feature in features:
|
||||
|
||||
Reference in New Issue
Block a user