2026-06-13 16:32:33 +02:00
|
|
|
# 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",
|
2026-06-13 16:32:33 +02:00
|
|
|
},
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-06-13 16:32:33 +02:00
|
|
|
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"
|