1029 lines
38 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
import re
from uuid import uuid4
from datetime import datetime, timedelta, timezone
from playwright.sync_api import expect
from tests.conftest import BASE_URL, assert_share_copies
from devplacepy.database import get_table
from devplacepy.utils import make_combined_slug
def _seed_posts(count):
suite = uuid4().hex[:8]
topic = f"pag{suite}"
users = get_table("users")
posts = get_table("posts")
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
for i in range(count):
owner = str(uuid4())
users.insert(
{
"uid": owner,
"username": f"pag_{suite}_{i}",
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"{suite}_{i}@test.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
content = f"Paginated post {i}"
posts.insert(
{
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(content, uid),
"title": None,
"content": content,
"topic": topic,
"project_uid": None,
"image": None,
"stars": 0,
"created_at": (base - timedelta(seconds=i)).isoformat(),
"deleted_at": None,
"deleted_by": None,
}
)
return topic
def _seed_post_with_comments(comment_texts, topic="devlog"):
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"seed_{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]}@seed.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
post_uid = str(uuid4())
marker = f"seedpost-{post_uid[:8]}"
get_table("posts").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": post_uid,
"user_uid": owner,
"slug": make_combined_slug(marker, post_uid),
"title": None,
"content": marker,
"topic": topic,
"project_uid": None,
"image": None,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
comments = get_table("comments")
base = datetime.now(timezone.utc)
for i, text in enumerate(comment_texts):
comments.insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": str(uuid4()),
"target_type": "post",
"target_uid": post_uid,
"post_uid": post_uid,
"user_uid": owner,
"content": text,
"parent_uid": None,
"created_at": (base + timedelta(seconds=i)).isoformat(),
}
)
return marker, post_uid
def _create_post_ui(page, content, topic="devlog"):
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.check(f"input[value='{topic}']")
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")
def _seed_searchable_posts():
token = uuid4().hex[:10]
users = get_table("users")
posts = get_table("posts")
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
match_title = f"Findable post {token}"
other_title = f"Unrelated post {uuid4().hex[:10]}"
for offset, title in ((0, match_title), (1, other_title)):
owner = str(uuid4())
users.insert(
{
"uid": owner,
"username": f"srch_{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(),
}
)
uid = str(uuid4())
posts.insert(
{
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(title, uid),
"title": title,
"content": f"Body for {title}",
"topic": "devlog",
"project_uid": None,
"image": None,
"stars": 0,
"created_at": (base - timedelta(seconds=offset)).isoformat(),
"deleted_at": None,
"deleted_by": None,
}
)
return token, match_title, other_title
import time
import requests
from tests.conftest import BASE_URL
_counter_project_files = [0]
def _signup_project_files():
_counter_project_files[0] += 1
name = f"pf{int(time.time() * 1000)}{_counter_project_files[0]}"
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,
)
key = get_table("users").find_one(username=name)["api_key"]
return name, key
def _h_project_files(key):
return {"X-API-KEY": key, "Accept": "application/json"}
def _create_project_project_files(key, title):
r = requests.post(
f"{BASE_URL}/projects/create",
headers=_h_project_files(key),
data={
"title": title,
"description": "filesystem test",
"project_type": "software",
"status": "In Development",
},
)
assert r.status_code == 200, r.text
return r.json()["data"]
def _write_project_files(key, slug, path, content):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/write",
headers=_h_project_files(key),
data={"path": path, "content": content},
allow_redirects=False,
)
def _mkdir(key, slug, path):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/mkdir",
headers=_h_project_files(key),
data={"path": path},
allow_redirects=False,
)
def _move(key, slug, from_path, to_path):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/move",
headers=_h_project_files(key),
data={"from_path": from_path, "to_path": to_path},
allow_redirects=False,
)
def _delete(key, slug, path):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/delete",
headers=_h_project_files(key),
data={"path": path},
allow_redirects=False,
)
def _list(slug, key=None):
return requests.get(
f"{BASE_URL}/projects/{slug}/files",
headers=_h_project_files(key) if key else {"Accept": "application/json"},
)
def _raw_project_files(slug, path, key=None):
return requests.get(
f"{BASE_URL}/projects/{slug}/files/raw",
params={"path": path},
headers=_h_project_files(key) if key else {"Accept": "application/json"},
)
def _paths(slug, key=None):
return sorted(f["path"] for f in _list(slug, key).json()["files"])
def _make_project_ui(page, title):
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", title)
page.fill("#description", "Project for filesystem UI test")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
return page.url
def _open_files(page, title):
proj_url = _make_project_ui(page, title)
slug = proj_url.rstrip("/").split("/")[-1]
page.goto(proj_url + "/files", wait_until="domcontentloaded")
return slug
def _dialog_fill(page, value):
page.locator(".dialog-overlay.visible .dialog-input").wait_for(state="visible")
page.fill(".dialog-overlay.visible .dialog-input", value)
page.click(".dialog-overlay.visible .dialog-confirm")
def _dialog_confirm(page):
page.locator(".dialog-overlay.visible .dialog-confirm").wait_for(state="visible")
page.click(".dialog-overlay.visible .dialog-confirm")
def _dialog_cancel(page):
page.locator(".dialog-overlay.visible .dialog-cancel").wait_for(state="visible")
page.click(".dialog-overlay.visible .dialog-cancel")
def _new_folder(page, name):
page.click("#pf-new-folder")
_dialog_fill(page, name)
page.wait_for_selector(f".pf-node-row:has-text('{name.split('/')[-1]}')")
def _new_file(page, name):
page.click("#pf-new-file")
_dialog_fill(page, name)
def _row(page, name):
return page.locator(f".pf-node-row:has-text('{name}')").first
def _alice_key():
return get_table("users").find_one(username="alice_test")["api_key"]
def _seed_projects(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)
projects = get_table("projects")
for i in range(count):
uid = str(uuid4())
title = f"Pag Project {i}"
projects.insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(title, uid),
"title": title,
"description": "paginated",
"project_type": "software",
"status": "In Development",
"stars": 0,
"created_at": (base - timedelta(seconds=i)).isoformat(),
}
)
return owner
def _create_project_projects(page, title):
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", title)
page.fill("#description", "Project for testing")
page.fill("#release_date", "01/06/2026")
page.check("input[value='game']")
page.locator("#platforms-input").fill("PC")
page.locator("#platforms-input").press("Enter")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
def _open_project_with_delete(page, title):
proj_url = _make_project_ui(page, title)
slug = proj_url.rstrip("/").split("/")[-1]
return proj_url, slug
def _click_delete(page):
page.locator(".project-actions-more").click()
page.locator(".context-menu-item:has-text('Delete')").click()
import asyncio
import io
import json
import zipfile
from pathlib import Path
from devplacepy.database import get_table, refresh_snapshot
from devplacepy.services.jobs import queue
from devplacepy.services.jobs.zip_service import ZipService
from tests.conftest import run_async
_counter_zip_download = [0]
def _signup_zip_download():
_counter_zip_download[0] += 1
name = f"zd{int(time.time() * 1000)}{_counter_zip_download[0]}"
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,
)
refresh_snapshot()
key = get_table("users").find_one(username=name)["api_key"]
return name, key
def _h_zip_download(key):
return {"X-API-KEY": key, "Accept": "application/json"}
def _create_project_zip_download(key, title):
r = requests.post(
f"{BASE_URL}/projects/create",
headers=_h_zip_download(key),
data={
"title": title,
"description": "zip download test",
"project_type": "software",
"status": "In Development",
},
)
assert r.status_code == 200, r.text
return r.json()["data"]
def _write_zip_download(key, slug, path, content):
r = requests.post(
f"{BASE_URL}/projects/{slug}/files/write",
headers=_h_zip_download(key),
data={"path": path, "content": content},
allow_redirects=False,
)
assert r.status_code in (200, 302), r.text
def _process_uid(uid):
async def drive():
svc = ZipService()
for _ in range(400):
await svc.run_once()
refresh_snapshot()
job = queue.get_job(uid)
if job and job["status"] in ("done", "failed"):
return
await asyncio.sleep(0.05)
run_async(drive())
def _drain_pending():
async def drive():
svc = ZipService()
appeared = False
for _ in range(600):
refresh_snapshot()
pending = [
r
for r in get_table("jobs").find(kind="zip")
if r["status"] in ("pending", "running")
]
if pending:
appeared = True
await svc.run_once()
refresh_snapshot()
pending = [
r
for r in get_table("jobs").find(kind="zip")
if r["status"] in ("pending", "running")
]
if appeared and not pending and not svc._inflight:
return
await asyncio.sleep(0.05)
run_async(drive())
def _cleanup_archives():
refresh_snapshot()
jobs = get_table("jobs")
for row in list(jobs.find(kind="zip")):
result = json.loads(row.get("result") or "{}")
local_path = result.get("local_path")
if local_path:
Path(local_path).unlink(missing_ok=True)
jobs.delete(uid=row["uid"])
def _login_zip_download(page, name):
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_url("**/feed", timeout=10000, wait_until="domcontentloaded")
def test_feed_topnav_navigation(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
page.click("a:has-text('Projects')")
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.click("a:has-text('Home')")
page.wait_for_url(f"{BASE_URL}/", wait_until="domcontentloaded")
def test_auto_open_file_on_load(alice):
page, _ = alice
key = _alice_key()
project = _create_project_project_files(key, "AutoOpen")
_write_project_files(key, project["slug"], "main.py", "print('hi')")
page.goto(
f"{BASE_URL}/projects/{project['slug']}/files", wait_until="domcontentloaded"
)
expect(page.locator("#pf-editor-wrap")).to_be_visible()
expect(page.locator("#pf-empty")).to_be_hidden()
def test_readme_preferred_on_load(alice):
page, _ = alice
key = _alice_key()
project = _create_project_project_files(key, "ReadmePref")
_write_project_files(key, project["slug"], "main.py", "x = 1")
_write_project_files(key, project["slug"], "README.md", "# Hello Readme")
page.goto(
f"{BASE_URL}/projects/{project['slug']}/files", wait_until="domcontentloaded"
)
page.wait_for_function(
"window.app && window.app.projectFiles && window.app.projectFiles.editor"
)
page.wait_for_timeout(300)
content = page.evaluate("window.app.projectFiles.editor.getValue()")
assert "Hello Readme" in content
def test_folder_click_keeps_editor(alice):
page, _ = alice
key = _alice_key()
project = _create_project_project_files(key, "FolderKeepsEditor")
_write_project_files(key, project["slug"], "src/a.py", "x = 1")
page.goto(
f"{BASE_URL}/projects/{project['slug']}/files", wait_until="domcontentloaded"
)
expect(page.locator("#pf-editor-wrap")).to_be_visible()
_row(page, "src").click()
expect(page.locator("#pf-editor-wrap")).to_be_visible()
expect(page.locator("#pf-empty")).to_be_hidden()
def test_project_vote(alice):
page, _ = alice
_create_project_projects(page, "Votable Project")
star = "form[action*='/votes/project/'] button"
count = "form[action*='/votes/project/'] .vote-count-value"
before = int(page.locator(star).first.inner_text().strip("☆ "))
page.locator(star).first.click()
expect(page.locator(count).first).to_have_text(str(before + 1))
after = int(page.locator(star).first.inner_text().strip("☆ "))
assert after == before + 1
def test_project_voted_state_persists(alice):
page, _ = alice
_create_project_projects(page, "Voted State Project")
star = "form[action*='/votes/project/'] 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_delete_own_project(alice):
page, _ = alice
_create_project_projects(page, "Deletable Project XYZ")
proj_url = page.url
page.locator(".project-actions-more").click()
page.locator(".context-menu-item:has-text('Delete')").click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
resp = page.goto(proj_url, wait_until="domcontentloaded")
assert resp.status == 404
def test_project_edit_button(alice):
page, _ = alice
_create_project_projects(page, "Editable Project ABC")
page.locator(".project-actions-more").click()
expect(page.locator(".context-menu-item:has-text('Edit')")).to_be_visible()
def test_project_edit_modal_prefills(alice):
page, _ = alice
_create_project_projects(page, "Edit Modal Project")
page.locator(".project-actions-more").click()
page.locator(".context-menu-item:has-text('Edit')").click()
assert page.is_visible("h3:has-text('Edit Project')")
expect(page.locator("#edit-project-title")).to_have_value("Edit Modal Project")
expect(page.locator("#platforms-tags .platform-tag:has-text('PC')")).to_be_visible()
def test_project_edit_submit(alice):
page, _ = alice
_create_project_projects(page, "Original Project Title")
proj_url = page.url
page.locator(".project-actions-more").click()
page.locator(".context-menu-item:has-text('Edit')").click()
page.fill("#edit-project-title", "Edited Project Title")
page.fill("#edit-project-description", "Edited project description body")
page.click("button:has-text('Save Changes')")
expect(
page.locator(".project-detail-title:has-text('Edited Project Title')")
).to_be_visible()
assert page.url.rstrip("/") == proj_url.rstrip("/")
def test_project_edit_status_change(alice):
page, _ = alice
_create_project_projects(page, "Status Change Project")
page.locator(".project-actions-more").click()
page.locator(".context-menu-item:has-text('Edit')").click()
page.check("#edit-project-modal input[value='Released']")
page.click("button:has-text('Save Changes')")
expect(page.locator(".project-status:has-text('Released')")).to_be_visible()
def test_project_edit_hidden_for_non_owner(alice, bob):
page, _ = alice
_create_project_projects(page, "Owner Only Edit Project")
proj_url = page.url
bob_page, _ = bob
bob_page.goto(proj_url, wait_until="domcontentloaded")
bob_page.locator(".project-actions-more").click()
expect(
bob_page.locator(".context-menu-item:has-text('Download zip')")
).to_be_visible()
expect(bob_page.locator(".context-menu-item:has-text('Edit')")).to_have_count(0)
def test_project_detail_share_button(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "Share Project")
page.fill("#description", "A project created for the share button test")
page.fill("#release_date", "01/06/2026")
page.check("input[value='game']")
page.locator("#platforms-input").fill("PC")
page.locator("#platforms-input").press("Enter")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
assert_share_copies(page, "/projects/")
def test_projects_page_loads(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
assert page.is_visible("h1:has-text('Projects')")
assert page.is_visible("text=Discover amazing projects")
def test_projects_search_bar(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
search = page.locator("input[placeholder='Search projects...']")
assert search.is_visible()
def test_projects_tabs(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
tabs = ["Recently Released", "Most Popular", "New This Week"]
for tab in tabs:
assert page.is_visible(f"text={tab}")
def test_projects_tab_switch(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.click("a:has-text('Most Popular')")
page.wait_for_url(f"{BASE_URL}/projects?tab=popular", wait_until="domcontentloaded")
page.click("a:has-text('New This Week')")
page.wait_for_url(f"{BASE_URL}/projects?tab=new", wait_until="domcontentloaded")
def test_create_project_button(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
fab = page.locator("#create-project-btn")
assert fab.is_visible()
fab.click()
assert page.is_visible("h3:has-text('Create Project')")
def test_create_project_full(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "Playwright Test Game")
page.fill("#description", "A game created by Playwright integration tests")
page.fill("#release_date", "01/06/2026")
page.check("input[value='game']")
page.locator("#platforms-input").fill("PC")
page.locator("#platforms-input").press("Enter")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
assert page.is_visible("text=Playwright Test Game")
def test_create_project_software(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "CLI Tool")
page.fill("#description", "A command line tool")
page.check("input[value='software']")
page.check("input[value='Released']")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
assert page.is_visible("text=CLI Tool")
def test_create_project_mobile_app(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "Mobile Messenger")
page.fill("#description", "A cross-platform messaging app")
page.check("input[value='mobile_app']")
page.locator("#platforms-input").fill("iOS")
page.locator("#platforms-input").press("Enter")
page.locator("#platforms-input").fill("Android")
page.locator("#platforms-input").press("Enter")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
assert page.is_visible("text=Mobile Messenger")
def test_project_search(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "SearchableProject")
page.fill("#description", "Find me via search")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.fill("input[placeholder='Search projects...']", "SearchableProject")
page.locator("input[placeholder='Search projects...']").press("Enter")
page.wait_for_timeout(500)
assert page.is_visible("text=SearchableProject")
def test_project_cancel_modal(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
cancel = page.locator("button:has-text('Cancel')").first
cancel.click()
modal = page.locator("#create-project-modal")
assert not modal.is_visible()
def test_project_platform_presets(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
presets = page.locator(".platform-preset")
assert presets.count() >= 5
presets.first.click()
tag = page.locator("#platforms-tags .platform-tag")
assert tag.is_visible()
def test_project_pagination_first_page(alice):
page, _ = alice
owner = _seed_projects(26)
page.goto(f"{BASE_URL}/projects?user_uid={owner}", wait_until="domcontentloaded")
assert page.locator(".project-card").count() == 25
assert page.is_visible(".load-more-wrap")
def test_project_pagination_load_more(alice):
page, _ = alice
owner = _seed_projects(26)
page.goto(f"{BASE_URL}/projects?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(".project-card").count() == 1
assert not page.is_visible(".load-more-wrap")
def test_project_pagination_count_reflects_total(alice):
page, _ = alice
owner = _seed_projects(26)
page.goto(f"{BASE_URL}/projects?user_uid={owner}", wait_until="domcontentloaded")
count_text = page.text_content(".projects-count")
assert "Showing 25 of 26 projects" in count_text
def test_projects_count(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
count_text = page.text_content(".projects-count")
assert "Showing" in count_text
assert "projects" in count_text
def test_project_detail_page(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "DetailTestProject")
page.fill("#description", "Project detail page test")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
assert page.is_visible("text=DetailTestProject")
assert page.is_visible("text=Back to Projects")
def test_project_detail_unauth(page):
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
assert page.is_visible("h1:has-text('Projects')")
assert page.locator("#create-project-btn").count() == 0
def test_project_detail_back_link(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "BackLinkProject")
page.fill("#description", "Test back link")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
back = page.locator("a:has-text('Back to Projects')")
assert back.is_visible()
back.click()
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
def test_project_comments_form_visible(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "CommentProject")
page.fill("#description", "Project for testing comments")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
assert page.is_visible("text=Comments")
assert page.is_visible("text=No comments yet")
assert page.is_visible("textarea[name='content']")
def test_project_comment_create(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "CommentCreateProj")
page.fill("#description", "Project for creating a comment")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
page.fill("textarea[name='content']", "Great project!")
page.click("button:has-text('Post')")
page.wait_for_timeout(500)
assert page.is_visible("text=Great project!")
def test_project_comment_reply(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "CommentReplyProj")
page.fill("#description", "Project for testing reply")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
page.fill("textarea[name='content']", "First comment")
page.click("button:has-text('Post')")
page.wait_for_timeout(500)
assert page.is_visible("text=First comment")
page.click("button:has-text('Reply')")
reply_form = page.locator(".comment-reply-form").first
reply_form.locator("textarea[name='content']").fill("Reply to comment")
reply_form.locator("button:has-text('Post')").click()
page.wait_for_timeout(500)
assert page.is_visible("text=Reply to comment")
def test_project_comment_delete(alice):
page, _ = alice
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
page.locator("#create-project-btn").click()
page.fill("#title", "CommentDeleteProj")
page.fill("#description", "Project for testing delete")
page.click("button:has-text('Create Project')")
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
page.fill("textarea[name='content']", "Comment to delete")
page.click("button:has-text('Post')")
page.wait_for_timeout(500)
assert page.is_visible("text=Comment to delete")
page.locator(".comment-action-btn:has-text('Delete')").click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
page.wait_for_timeout(500)
assert not page.is_visible("text=Comment to delete")
def test_project_make_private(alice):
page, _ = alice
_create_project_projects(page, "PrivateToggleProject")
slug = page.url.rstrip("/").split("/")[-1]
page.locator(".project-actions-more").click()
page.locator(".context-menu-item:has-text('Make private')").click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
page.wait_for_url(f"**/projects/{slug}", wait_until="domcontentloaded")
expect(page.locator(".badge-type:has-text('Private')")).to_be_visible()
def test_project_make_readonly(alice):
page, _ = alice
_create_project_projects(page, "ReadonlyToggleProject")
slug = page.url.rstrip("/").split("/")[-1]
page.locator(".project-actions-more").click()
page.locator(".context-menu-item:has-text('Make read-only')").click()
page.locator(".dialog-overlay.visible .dialog-confirm").click()
page.wait_for_url(f"**/projects/{slug}", wait_until="domcontentloaded")
expect(page.locator(".badge-type:has-text('Read-only')")).to_be_visible()
def test_data_confirm_accept_proceeds(alice):
page, _ = alice
proj_url, _ = _open_project_with_delete(page, "Confirm Accept")
_click_delete(page)
page.locator(".dialog-overlay.visible .dialog-confirm").click()
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
resp = page.goto(proj_url, wait_until="domcontentloaded")
assert resp.status == 404
def test_detail_download_button_downloads(app_server, page):
try:
name, key = _signup_zip_download()
proj = _create_project_zip_download(key, "Detail Download UI")
slug = proj["slug"] or proj["uid"]
_write_zip_download(key, slug, "README.md", "# hi")
_login_zip_download(page, name)
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
page.locator(".project-actions-more").click()
btn = page.locator(".context-menu-item:has-text('Download zip')")
btn.wait_for(state="visible")
with page.expect_download(timeout=30000) as dl_info:
btn.click()
_drain_pending()
assert dl_info.value.suggested_filename.endswith(".zip")
finally:
_cleanup_archives()
def _seed_project_with_comments():
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"pcseed_{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]}@pc.seed",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
marker = f"projcom-{uid[:8]}"
get_table("projects").insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(marker, uid),
"title": marker,
"description": "Project with comment hierarchy.",
"project_type": "software",
"platforms": "Linux",
"status": "Released",
"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": "project",
"target_uid": uid,
"user_uid": owner,
"content": content,
"parent_uid": par,
"created_at": (base + timedelta(seconds=off)).isoformat(),
}
)
return marker
def test_projects_list_preserves_comment_hierarchy(page, app_server):
marker = _seed_project_with_comments()
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
card = page.locator(".project-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-07-07 15:28:28 +02:00
import time as _time_containers_menu
import requests as _requests_containers_menu
from tests.conftest import login_user
from devplacepy.database import get_primary_admin_uid, invalidate_admins_cache
from devplacepy.utils import clear_user_cache
_counter_containers_menu = [0]
def _signup_containers_menu(prefix):
_counter_containers_menu[0] += 1
name = f"{prefix}{int(_time_containers_menu.time() * 1000)}{_counter_containers_menu[0]}"
_requests_containers_menu.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",
2026-07-07 15:28:28 +02:00
},
allow_redirects=True,
)
row = get_table("users").find_one(username=name)
return name, row["uid"], row["api_key"]
def _make_admin_containers_menu(prefix):
name, uid, key = _signup_containers_menu(prefix)
get_table("users").update({"uid": uid, "role": "Admin"}, ["uid"])
clear_user_cache(uid)
invalidate_admins_cache()
return name, uid, key
def _create_project_containers_menu(key, title, is_private=False):
data = {
"title": title,
"description": "containers menu gating test",
"project_type": "software",
"status": "In Development",
}
if is_private:
data["is_private"] = "on"
r = _requests_containers_menu.post(
f"{BASE_URL}/projects/create",
headers={"Accept": "application/json", "X-API-KEY": key},
data=data,
)
assert r.status_code == 200, r.text
return r.json()["data"]["slug"]
def test_containers_menu_visible_for_admin_on_own_private_project(page, app_server):
name, uid, key = _make_admin_containers_menu("cmown")
slug = _create_project_containers_menu(key, "Containers Menu Own Private", is_private=True)
login_user(page, {"email": f"{name}@t.dev", "password": "secret123"})
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
page.locator(".project-actions-more").click()
expect(page.locator(".context-menu-item:has-text('Containers')")).to_be_visible()
def test_containers_menu_visible_for_any_admin_on_public_project(page, app_server):
_, _, owner_key = _make_admin_containers_menu("cmpubowner")
other_name, _, _ = _make_admin_containers_menu("cmpubother")
slug = _create_project_containers_menu(owner_key, "Containers Menu Public", is_private=False)
login_user(page, {"email": f"{other_name}@t.dev", "password": "secret123"})
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
page.locator(".project-actions-more").click()
expect(page.locator(".context-menu-item:has-text('Containers')")).to_be_visible()
def test_containers_menu_hidden_for_non_owner_admin_on_member_private_project(page, app_server):
_, member_uid, member_key = _signup_containers_menu("cmmember")
slug = _create_project_containers_menu(member_key, "Containers Menu Member Private", is_private=True)
admin_name, admin_uid, _ = _make_admin_containers_menu("cmviewer")
assert get_primary_admin_uid() != admin_uid
login_user(page, {"email": f"{admin_name}@t.dev", "password": "secret123"})
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
page.locator(".project-actions-more").click()
expect(page.locator(".context-menu-item:has-text('Containers')")).to_have_count(0)