161 lines
5.7 KiB
Python
Raw Normal View History

# retoor <retoor@molodetz.nl>
import time
import pytest
import requests
from tests.conftest import BASE_URL
from devplacepy.database import get_table
from devplacepy.utils import clear_user_cache
from devplacepy import project_files
from devplacepy.project_files import ProjectFileError
from devplacepy.services.devii.actions.dispatcher import (
confirmation_error,
_is_confirmed,
)
_counter_project_visibility = [0]
def _signup_project_visibility():
_counter_project_visibility[0] += 1
name = f"pv{int(time.time() * 1000)}{_counter_project_visibility[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,
)
row = get_table("users").find_one(username=name)
return name, row["uid"], row["api_key"]
def _make_admin_project_visibility():
name, uid, key = _signup_project_visibility()
get_table("users").update({"uid": uid, "role": "Admin"}, ["uid"])
clear_user_cache(uid)
return name, uid, key
def _h_project_visibility(key=None):
headers = {"Accept": "application/json"}
if key:
headers["X-API-KEY"] = key
return headers
def _create_project_project_visibility(key, title, is_private=False):
data = {
"title": title,
"description": "visibility test",
"project_type": "software",
"status": "In Development",
}
if is_private:
data["is_private"] = "on"
r = requests.post(f"{BASE_URL}/projects/create", headers=_h_project_visibility(key), data=data)
assert r.status_code == 200, r.text
return r.json()["data"]
def _project_uid(slug):
return get_table("projects").find_one(slug=slug)["uid"]
def _write_project_visibility(key, slug, path, content):
return requests.post(
f"{BASE_URL}/projects/{slug}/files/write",
headers=_h_project_visibility(key),
data={"path": path, "content": content},
allow_redirects=False,
)
def _set_private(key, slug, value):
return requests.post(
f"{BASE_URL}/projects/{slug}/private",
headers=_h_project_visibility(key),
data={"value": 1 if value else 0},
allow_redirects=False,
)
def _set_readonly(key, slug, value):
return requests.post(
f"{BASE_URL}/projects/{slug}/readonly",
headers=_h_project_visibility(key),
data={"value": 1 if value else 0},
allow_redirects=False,
)
def _list_slugs(key=None, user_uid=None):
params = {"user_uid": user_uid} if user_uid else None
r = requests.get(f"{BASE_URL}/projects", headers=_h_project_visibility(key), params=params)
return [p["slug"] for p in r.json()["projects"]]
def test_owner_can_edit_project(app_server):
_, _, key = _signup_project_visibility()
slug = _create_project_project_visibility(key, "Editable Via Api")["slug"]
r = requests.post(
f"{BASE_URL}/projects/edit/{slug}",
headers=_h_project_visibility(key),
data={
"title": "Edited Via Api",
"description": "updated description body",
"project_type": "website",
"status": "Released",
"platforms": "Linux,Web",
},
allow_redirects=False,
)
assert r.status_code == 200 and r.json()["ok"] is True
row = get_table("projects").find_one(slug=slug)
assert row["title"] == "Edited Via Api"
assert row["status"] == "Released"
assert row["project_type"] == "website"
assert row["platforms"] == "Linux,Web"
Dedicate the project page to the project The project detail page becomes a full project showcase built entirely from existing platform mechanisms. One encompassing dark card wraps the page; inner panels (tab bar, sidebar cards, devlog entries, comments) sit one elevation lighter. The hero opens with a cover banner and an optional logo tile, both plain attachment references (cover_attachment_uid/logo_attachment_uid) uploaded through the standard dp-upload attachment widget and linked via the existing link_attachments choke point - the route validates each uid belongs to the actor and is an image, and an empty value on edit keeps the current one. The title block, type/platform chips and author row overlay the banner behind a scrim with a dark text shadow, next to an owner-set Visit Website CTA; website_url and repo_url are normalized in models and render with rel noopener nofollow. An anchor tab bar (Overview, Devlog, Screenshots when present, Comments, Files) navigates the page. The main column keeps About, the devlog timeline (with devlog_count and an owner Post update button opening the shared composer preset to the devlog topic + project - the form now lives once in _post_composer_form.html, included by feed.html and project_detail.html), a Screenshots gallery built from image attachments minus the cover/logo (thumbnails, lightbox, 12 rendered), and the comment thread; the sidebar holds Links, Stats and the Author card. Owners add gallery images from the More menu via POST /projects/{slug}/screenshots (owner-only, audit project.screenshots.add, Devii action project_add_screenshots, docs id projects-screenshots). comment_count/devlog_count ride ProjectDetailOut, the new fields ride ProjectOut, and the create/edit faces (modals, Devii actions, API docs) carry them. The project comment/files e2e tests scope their locators per the documented dual-control idiom, and new unit/api/e2e tests cover URL normalization, the counts, the hero attachment guard, the screenshots flow and the preset composer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 23:00:12 +02:00
def test_owner_can_set_and_clear_link_urls(app_server):
_, _, key = _signup_project_visibility()
slug = _create_project_project_visibility(key, "Website Via Api")["slug"]
r = requests.post(
f"{BASE_URL}/projects/edit/{slug}",
headers=_h_project_visibility(key),
data={
"title": "Website Via Api",
"description": "has links now",
"website_url": "myproject.dev/docs",
"repo_url": "github.com/me/website-via-api",
},
allow_redirects=False,
)
assert r.status_code == 200 and r.json()["ok"] is True
row = get_table("projects").find_one(slug=slug)
assert row["website_url"] == "https://myproject.dev/docs"
assert row["repo_url"] == "https://github.com/me/website-via-api"
html = requests.get(f"{BASE_URL}/projects/{slug}").text
assert "Visit Website" in html
assert "Repository" in html
r = requests.post(
f"{BASE_URL}/projects/edit/{slug}",
headers=_h_project_visibility(key),
data={
"title": "Website Via Api",
"description": "links removed",
"website_url": "",
"repo_url": "",
},
allow_redirects=False,
)
assert r.status_code == 200
row = get_table("projects").find_one(slug=slug)
assert row["website_url"] is None
assert row["repo_url"] is None
assert "Visit Website" not in requests.get(f"{BASE_URL}/projects/{slug}").text
def test_non_owner_cannot_edit_project(app_server):
_, _, owner_key = _signup_project_visibility()
slug = _create_project_project_visibility(owner_key, "Owner Edit Guard")["slug"]
_, _, other_key = _signup_project_visibility()
r = requests.post(
f"{BASE_URL}/projects/edit/{slug}",
headers=_h_project_visibility(other_key),
data={"title": "Hijacked", "description": "should not persist"},
allow_redirects=False,
)
assert r.status_code == 403
assert get_table("projects").find_one(slug=slug)["title"] == "Owner Edit Guard"