523 lines
18 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
import re
import time
import requests
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from playwright.sync_api import expect
2026-08-09 11:39:53 +02:00
from tests.conftest import (
BASE_URL,
assert_no_horizontal_overflow,
assert_share_copies,
)
from devplacepy.database import get_table
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
def test_gist_listing_loads(page, app_server):
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
assert page.is_visible("h1:has-text('Gists')")
assert page.is_visible("text=Share and discover code snippets")
def test_gist_listing_empty(page, app_server):
# filter to a user with no gists so the empty state is deterministic even when
# other tests (sharing the session DB) have created gists
page.goto(
f"{BASE_URL}/gists?user_uid=no-such-user-xyz", wait_until="domcontentloaded"
)
assert page.is_visible("text=No gists found")
def test_gist_listing_shows_sidebar(page, app_server):
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
assert page.is_visible("text=Languages")
assert page.is_visible("text=All")
def test_guest_cannot_create(page, app_server):
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
assert not page.is_visible("#create-gist-btn")
def test_create_gist(alice, app_server):
page, _ = alice
title = f"Create Test {int(time.time())}"
_create_gist(page, title=title, source_code="print('create test')")
assert page.is_visible(f"text={title}")
assert page.is_visible("text=Python")
assert page.is_visible("text=print('create test')")
def test_gist_detail_shows_all_sections(alice, app_server):
page, _ = alice
title = f"Detail Test {int(time.time())}"
_create_gist(
page,
title=title,
description="See all sections",
source_code="def hello(): pass",
)
assert page.is_visible("text=See all sections")
assert page.is_visible("text=def hello(): pass")
assert page.is_visible("text=Copy")
assert page.is_visible("text=Back to Gists")
def test_gist_detail_back_link(alice, app_server):
page, _ = alice
title = f"Back Link Test {int(time.time())}"
_create_gist(page, title=title, source_code="x = 1")
page.click("text=Back to Gists")
page.wait_for_url(f"{BASE_URL}/gists", timeout=10000, wait_until="domcontentloaded")
assert page.is_visible("h1:has-text('Gists')")
def test_edit_gist(alice, app_server):
page, _ = alice
title = f"Edit Test Original {int(time.time())}"
_create_gist(page, title=title, source_code="x = 1")
page.locator("button:has-text('Edit')").wait_for(state="visible", timeout=5000)
page.click("button:has-text('Edit')")
page.wait_for_timeout(800)
new_title = f"Edit Test Updated {int(time.time())}"
page.fill("#edit-gist-title", new_title)
page.select_option("#edit-gist-language", "javascript")
_set_cm_value(page, "const x = 1;")
page.wait_for_timeout(300)
page.locator("button.btn-primary:has-text('Save Changes')").click()
page.wait_for_url("**/gists/*", timeout=15000, wait_until="domcontentloaded")
assert page.is_visible(f"text={new_title}")
assert page.is_visible("text=JavaScript")
def test_non_owner_cannot_edit_or_delete(bob, alice, app_server):
bob_page, _ = bob
alice_page, alice_user = alice
title = f"Owner Check {int(time.time())}"
_create_gist(alice_page, title=title, source_code="owner_only = True")
slug = alice_page.url.split("/gists/")[-1]
bob_page.goto(f"{BASE_URL}/gists/{slug}", wait_until="domcontentloaded")
assert not bob_page.is_visible("button:has-text('Edit')")
assert not bob_page.is_visible("button:has-text('Delete')")
def test_delete_gist(alice, app_server):
page, _ = alice
title = f"Delete Test {int(time.time())}"
_create_gist(page, title=title, source_code="delete_me = True")
page.locator("button:has-text('Delete')").wait_for(state="visible", timeout=5000)
page.click("button:has-text('Delete')")
page.locator(".dialog-overlay.visible .dialog-confirm").click()
page.wait_for_url(f"{BASE_URL}/gists", timeout=10000, wait_until="domcontentloaded")
def test_gist_voting(alice, app_server):
page, _ = alice
title = f"Vote Test {int(time.time())}"
_create_gist(page, title=title, source_code="vote_me = True")
star_btn = page.locator("form[action*='/votes/gist/'] button").first
original_text = star_btn.text_content()
original_stars = int(original_text.strip("\u2606 "))
star_btn.click()
page.wait_for_timeout(500)
star_btn = page.locator("form[action*='/votes/gist/'] button").first
new_text = star_btn.text_content()
new_stars = int(new_text.strip("\u2606 "))
assert new_stars == original_stars + 1
def test_gist_detail_has_sourcecode_schema(alice, app_server):
page, _ = alice
_create_gist(page, title=f"Schema Gist {int(time.time())}", source_code="x = 1")
scripts = page.locator('script[type="application/ld+json"]')
text = " ".join(scripts.nth(i).text_content() for i in range(scripts.count()))
assert "SoftwareSourceCode" in text
def test_gist_detail_share_button(alice, app_server):
page, _ = alice
_create_gist(page, title=f"Share Gist {int(time.time())}", source_code="x = 1")
assert_share_copies(page, "/gists/")
def test_gist_code_copy_button(alice, app_server):
from playwright.sync_api import expect
page, _ = alice
_create_gist(page, title=f"Copy Gist {int(time.time())}", source_code="copyme = 42")
btn = page.locator("button[data-copy='gist-code-content']")
btn.click()
expect(btn).to_have_text("Copied!", timeout=3000)
def test_language_filter(alice, app_server):
page, _ = alice
title = f"Lang Filter Test {int(time.time())}"
_create_gist(page, title=title, language="go", source_code="package main")
page.goto(f"{BASE_URL}/gists?language=go", wait_until="domcontentloaded")
assert page.is_visible(f"text={title}")
page.goto(f"{BASE_URL}/gists?language=python", wait_until="domcontentloaded")
assert not page.is_visible(f"text={title}")
def test_gists_search_bar_visible(alice):
page, _ = alice
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
search = page.locator("input[name='search'][placeholder='Search gists...']")
assert search.is_visible()
def test_gists_search_filters(alice):
page, _ = alice
token, match_title, other_title = _seed_searchable_gists()
page.goto(f"{BASE_URL}/gists?search={token}", wait_until="domcontentloaded")
assert page.locator(f".gist-card-title:has-text('{match_title}')").count() == 1
assert page.locator(f".gist-card-title:has-text('{other_title}')").count() == 0
def test_gists_search_no_match(alice):
page, _ = alice
_seed_searchable_gists()
page.goto(
f"{BASE_URL}/gists?search=zzz{uuid4().hex}", wait_until="domcontentloaded"
)
assert page.locator(".gist-card-title").count() == 0
def test_gists_search_preserves_language(alice):
page, _ = alice
token, match_title, _ = _seed_searchable_gists(language="go")
page.goto(
f"{BASE_URL}/gists?language=go&search={token}", wait_until="domcontentloaded"
)
assert page.locator(f".gist-card-title:has-text('{match_title}')").count() == 1
preserved = page.locator(
".sidebar-card form input[type='hidden'][name='language'][value='go']"
)
assert preserved.count() == 1
def test_gist_comments(alice, app_server):
page, _ = alice
title = f"Comment Test {int(time.time())}"
_create_gist(page, title=title, source_code="has_comments = True")
comment_text = f"Nice gist! {int(time.time())}"
comment_input = page.locator("textarea[name='content']").first
comment_input.wait_for(state="visible", timeout=5000)
comment_input.fill(comment_text)
page.locator("button.comment-form-submit").first.click()
page.wait_for_url("**/gists/*", timeout=10000, wait_until="domcontentloaded")
assert page.is_visible(f"text={comment_text}")
def test_gist_listing_shows_created_gist(alice, app_server):
page, _ = alice
title = f"Listing Shows {int(time.time())}"
_create_gist(page, title=title, source_code="show_in_list = True")
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
assert page.is_visible(f"text={title}")
def test_gist_pagination_first_page(alice):
page, _ = alice
owner = _seed_gists(26)
page.goto(f"{BASE_URL}/gists?user_uid={owner}", wait_until="domcontentloaded")
assert page.locator(".gist-card").count() == 25
assert page.is_visible(".load-more-wrap")
def test_gist_pagination_load_more(alice):
page, _ = alice
owner = _seed_gists(26)
page.goto(f"{BASE_URL}/gists?user_uid={owner}", wait_until="domcontentloaded")
page.click(".load-more-wrap a")
page.wait_for_url(lambda url: "before=" in url, wait_until="domcontentloaded")
assert f"user_uid={owner}" in page.url
assert page.locator(".gist-card").count() == 1
assert not page.is_visible(".load-more-wrap")
def test_gist_no_pagination_below_page_size(alice):
page, _ = alice
owner = _seed_gists(10)
page.goto(f"{BASE_URL}/gists?user_uid={owner}", wait_until="domcontentloaded")
assert page.locator(".gist-card").count() == 10
assert not page.is_visible(".load-more-wrap")
def test_gist_voted_state_persists(alice):
page, _ = alice
_create_gist(page, title="Voted State Gist")
star = "form[action*='/votes/gist/'] button"
page.locator(star).first.click()
page.wait_for_timeout(500)
page.reload(wait_until="domcontentloaded")
expect(page.locator(star).first).to_have_class(re.compile(r"\bvoted\b"))
def test_oversized_gist_shows_client_error(alice, app_server):
page, _ = alice
title = f"Client Oversize {int(time.time())}"
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.wait_for_timeout(300)
_set_cm_value(page, _make_source(SOURCE_LENGTH_LIMIT + 1))
page.wait_for_timeout(300)
page.locator("button.btn-primary:has-text('Create Gist')").click()
error = page.locator(".gist-length-error")
error.wait_for(state="visible", timeout=5000)
assert "maximum" in error.text_content()
assert page.url.rstrip("/") == f"{BASE_URL}/gists"
assert get_table("gists").find_one(title=title) is None
def test_create_large_gist_via_ui_saves(alice, app_server):
page, _ = alice
title = f"UI Large {int(time.time())}"
source = _make_source(DPBOT_SIZE)
_create_gist(page, title=title, description="ui large", source_code=source)
assert "/gists/" in page.url
saved = get_table("gists").find_one(title=title)
assert saved is not None
assert len(saved["source_code"]) > DPBOT_SIZE // 2
def _seed_gist_with_comments():
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"gcseed_{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]}@gc.seed",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
marker = f"gistcom-{uid[:8]}"
get_table("gists").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(marker, uid),
"title": marker,
"description": "Gist with comment hierarchy.",
"source_code": "print('x')",
"language": "python",
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
parent = str(uuid4())
base = datetime.now(timezone.utc)
rows = [
(f"{marker}-excluded", str(uuid4()), None, -2),
(f"{marker}-flat", str(uuid4()), None, -1),
(f"{marker}-parent", parent, None, 0),
(f"{marker}-reply", str(uuid4()), parent, 1),
]
comments = get_table("comments")
for content, cuid, par, off in rows:
comments.insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": cuid,
"target_type": "gist",
"target_uid": uid,
"user_uid": owner,
"content": content,
"parent_uid": par,
"created_at": (base + timedelta(seconds=off)).isoformat(),
}
)
return marker
def test_gists_list_preserves_comment_hierarchy(page, app_server):
marker = _seed_gist_with_comments()
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
card = page.locator(".gist-card").filter(has_text=marker).first
card.wait_for(state="visible")
previews = card.locator(".post-card-comments .comment-text")
expect(previews).to_have_count(3)
expect(card.locator(".post-card-comments")).not_to_contain_text(f"{marker}-excluded")
nested = card.locator(".post-card-comments .comment-replies .comment-text")
expect(nested).to_contain_text(f"{marker}-reply")
2026-08-09 11:39:53 +02:00
def _seed_gist_with_long_code_line():
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"gcode_{owner[:8]}",
"email": f"{owner[:8]}@gcode.seed",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
marker = f"gistcode-{uid[:8]}"
line = "unbreakable_identifier_" * 8
get_table("gists").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(marker, uid),
"title": marker,
"description": f"```python\n{line}\n```",
"source_code": "print('x')",
"language": "python",
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return marker
def test_gists_long_code_line_does_not_widen_layout(page, app_server):
marker = _seed_gist_with_long_code_line()
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
card = page.locator(".gist-card").filter(has_text=marker).first
card.wait_for(state="visible")
assert card.locator("pre").count() == 1
assert_no_horizontal_overflow(page, ".gists-layout")