2026-06-13 16:32:33 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
2026-07-07 15:28:28 +02:00
|
|
|
from datetime import datetime, timedelta, timezone
|
2026-06-13 16:32:33 +02:00
|
|
|
from devplacepy.content import (
|
|
|
|
|
is_owner,
|
2026-06-17 16:08:28 +02:00
|
|
|
can_view_project,
|
2026-07-07 15:28:28 +02:00
|
|
|
can_manage_instance,
|
|
|
|
|
can_view_instance,
|
|
|
|
|
can_view_project_containers,
|
2026-06-13 16:32:33 +02:00
|
|
|
canonical_redirect,
|
|
|
|
|
first_image_url,
|
|
|
|
|
enrich_items,
|
2026-07-07 15:28:28 +02:00
|
|
|
owns_instance,
|
2026-06-13 16:32:33 +02:00
|
|
|
)
|
2026-07-07 15:28:28 +02:00
|
|
|
from devplacepy.database import get_table, get_users_by_uids, invalidate_admins_cache
|
2026-06-13 16:32:33 +02:00
|
|
|
from devplacepy.utils import generate_uid
|
|
|
|
|
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_is_owner_matches_user_uid():
|
|
|
|
|
assert is_owner({"user_uid": "u1"}, {"uid": "u1"}) is True
|
|
|
|
|
assert is_owner({"user_uid": "u1"}, {"uid": "u2"}) is False
|
|
|
|
|
assert is_owner(None, {"uid": "u1"}) is False
|
|
|
|
|
assert is_owner({"user_uid": "u1"}, None) is False
|
|
|
|
|
|
|
|
|
|
|
2026-06-17 16:08:28 +02:00
|
|
|
def _make_user(role="Member"):
|
|
|
|
|
uid = generate_uid()
|
|
|
|
|
username = f"cv_{uid[:8]}"
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"username": username,
|
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",
|
2026-06-17 16:08:28 +02:00
|
|
|
"email": f"{username}@t.dev",
|
|
|
|
|
"role": role,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return get_table("users").find_one(uid=uid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_project_public_visible_to_everyone(local_db):
|
|
|
|
|
owner = _make_user()
|
|
|
|
|
other = _make_user()
|
|
|
|
|
admin = _make_user("Admin")
|
|
|
|
|
project = {"user_uid": owner["uid"], "is_private": 0}
|
|
|
|
|
assert can_view_project(project, owner) is True
|
|
|
|
|
assert can_view_project(project, other) is True
|
|
|
|
|
assert can_view_project(project, admin) is True
|
|
|
|
|
assert can_view_project(project, None) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_member_private_visible_to_owner_and_admin(local_db):
|
|
|
|
|
owner = _make_user()
|
|
|
|
|
other = _make_user()
|
|
|
|
|
admin = _make_user("Admin")
|
|
|
|
|
project = {"user_uid": owner["uid"], "is_private": 1}
|
|
|
|
|
assert can_view_project(project, owner) is True
|
|
|
|
|
assert can_view_project(project, admin) is True
|
|
|
|
|
assert can_view_project(project, other) is False
|
|
|
|
|
assert can_view_project(project, None) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_admin_private_visible_to_owner_admin_only(local_db):
|
|
|
|
|
owner_admin = _make_user("Admin")
|
|
|
|
|
other_admin = _make_user("Admin")
|
|
|
|
|
member = _make_user()
|
|
|
|
|
project = {"user_uid": owner_admin["uid"], "is_private": 1}
|
|
|
|
|
assert can_view_project(project, owner_admin) is True
|
|
|
|
|
assert can_view_project(project, other_admin) is False
|
|
|
|
|
assert can_view_project(project, member) is False
|
|
|
|
|
assert can_view_project(project, None) is False
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 15:28:28 +02:00
|
|
|
def _make_user_at(role, created_at):
|
|
|
|
|
uid = generate_uid()
|
|
|
|
|
username = f"cv_{uid[:8]}"
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"username": username,
|
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",
|
2026-07-07 15:28:28 +02:00
|
|
|
"email": f"{username}@t.dev",
|
|
|
|
|
"role": role,
|
|
|
|
|
"created_at": created_at.isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
invalidate_admins_cache()
|
|
|
|
|
return get_table("users").find_one(uid=uid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _demote_existing_admins():
|
|
|
|
|
existing = [r["uid"] for r in get_table("users").find(role="Admin")]
|
|
|
|
|
for uid in existing:
|
|
|
|
|
get_table("users").update({"uid": uid, "role": "Member"}, ["uid"])
|
|
|
|
|
invalidate_admins_cache()
|
|
|
|
|
return existing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _restore_admins(uids):
|
|
|
|
|
for uid in uids:
|
|
|
|
|
get_table("users").update({"uid": uid, "role": "Admin"}, ["uid"])
|
|
|
|
|
invalidate_admins_cache()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _purge_users(*rows):
|
|
|
|
|
for row in rows:
|
|
|
|
|
if row:
|
|
|
|
|
get_table("users").delete(uid=row["uid"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_owns_instance_true_for_creator():
|
|
|
|
|
owner = {"uid": "cnt-u1"}
|
|
|
|
|
other = {"uid": "cnt-u2"}
|
|
|
|
|
instance = {"created_by": owner["uid"]}
|
2026-07-26 14:57:18 +02:00
|
|
|
project = {"user_uid": "cnt-u3"}
|
2026-07-07 15:28:28 +02:00
|
|
|
assert owns_instance(instance, project, owner) is True
|
|
|
|
|
assert owns_instance(instance, project, other) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_owns_instance_true_for_project_owner():
|
|
|
|
|
owner = {"uid": "cnt-u1"}
|
|
|
|
|
creator = {"uid": "cnt-u2"}
|
|
|
|
|
instance = {"created_by": creator["uid"]}
|
|
|
|
|
project = {"user_uid": owner["uid"]}
|
|
|
|
|
assert owns_instance(instance, project, owner) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_owns_instance_false_when_missing_data():
|
|
|
|
|
assert owns_instance(None, {}, {"uid": "cnt-u1"}) is False
|
|
|
|
|
assert owns_instance({"created_by": "cnt-u1"}, {}, None) is False
|
|
|
|
|
assert owns_instance({"created_by": "cnt-u1"}, {}, {}) is False
|
|
|
|
|
assert owns_instance({"created_by": "cnt-u1"}, None, {"uid": "cnt-u2"}) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_project_containers_requires_admin(local_db):
|
|
|
|
|
owner = _make_user()
|
|
|
|
|
project = {"user_uid": owner["uid"], "is_private": 0}
|
|
|
|
|
try:
|
|
|
|
|
assert can_view_project_containers(project, owner) is False
|
|
|
|
|
assert can_view_project_containers(project, None) is False
|
|
|
|
|
assert can_view_project_containers(None, owner) is False
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(owner)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_project_containers_public_visible_to_any_admin(local_db):
|
|
|
|
|
restore = _demote_existing_admins()
|
|
|
|
|
owner_admin = other_admin = None
|
|
|
|
|
try:
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
owner_admin = _make_user_at("Admin", base)
|
|
|
|
|
other_admin = _make_user_at("Admin", base + timedelta(seconds=1))
|
|
|
|
|
project = {"user_uid": owner_admin["uid"], "is_private": 0}
|
|
|
|
|
assert can_view_project_containers(project, owner_admin) is True
|
|
|
|
|
assert can_view_project_containers(project, other_admin) is True
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(owner_admin, other_admin)
|
|
|
|
|
_restore_admins(restore)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_project_containers_private_hidden_from_other_admin(local_db):
|
|
|
|
|
restore = _demote_existing_admins()
|
|
|
|
|
owner_admin = other_admin = None
|
|
|
|
|
try:
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
owner_admin = _make_user_at("Admin", base)
|
|
|
|
|
other_admin = _make_user_at("Admin", base + timedelta(seconds=1))
|
|
|
|
|
project = {"user_uid": owner_admin["uid"], "is_private": 1}
|
|
|
|
|
assert can_view_project_containers(project, owner_admin) is True
|
|
|
|
|
assert can_view_project_containers(project, other_admin) is False
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(owner_admin, other_admin)
|
|
|
|
|
_restore_admins(restore)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_project_containers_primary_admin_sees_all(local_db):
|
|
|
|
|
restore = _demote_existing_admins()
|
|
|
|
|
primary = other_admin = None
|
|
|
|
|
try:
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
primary = _make_user_at("Admin", base)
|
|
|
|
|
other_admin = _make_user_at("Admin", base + timedelta(seconds=1))
|
|
|
|
|
project = {"user_uid": other_admin["uid"], "is_private": 1}
|
|
|
|
|
assert can_view_project_containers(project, primary) is True
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(primary, other_admin)
|
|
|
|
|
_restore_admins(restore)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_instance_requires_admin(local_db):
|
|
|
|
|
owner = _make_user()
|
|
|
|
|
instance = {"created_by": owner["uid"], "project_uid": "p1"}
|
|
|
|
|
project = {"user_uid": owner["uid"], "is_private": 0}
|
|
|
|
|
try:
|
|
|
|
|
assert can_view_instance(instance, project, owner) is False
|
|
|
|
|
assert can_manage_instance(instance, project, owner) is False
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(owner)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_instance_owner_sees_own_private_hidden_from_other_admin(local_db):
|
|
|
|
|
restore = _demote_existing_admins()
|
|
|
|
|
owner_admin = other_admin = None
|
|
|
|
|
try:
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
owner_admin = _make_user_at("Admin", base)
|
|
|
|
|
other_admin = _make_user_at("Admin", base + timedelta(seconds=1))
|
|
|
|
|
project = {"user_uid": owner_admin["uid"], "is_private": 1}
|
|
|
|
|
instance = {"created_by": owner_admin["uid"], "project_uid": "p1"}
|
|
|
|
|
assert can_view_instance(instance, project, owner_admin) is True
|
|
|
|
|
assert can_view_instance(instance, project, other_admin) is False
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(owner_admin, other_admin)
|
|
|
|
|
_restore_admins(restore)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_instance_public_project_visible_to_any_admin(local_db):
|
|
|
|
|
restore = _demote_existing_admins()
|
|
|
|
|
creator = other_admin = None
|
|
|
|
|
try:
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
creator = _make_user_at("Admin", base)
|
|
|
|
|
other_admin = _make_user_at("Admin", base + timedelta(seconds=1))
|
|
|
|
|
project = {"user_uid": creator["uid"], "is_private": 0}
|
|
|
|
|
instance = {"created_by": creator["uid"], "project_uid": "p1"}
|
|
|
|
|
assert can_view_instance(instance, project, other_admin) is True
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(creator, other_admin)
|
|
|
|
|
_restore_admins(restore)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_view_instance_primary_admin_sees_others_private_instance(local_db):
|
|
|
|
|
restore = _demote_existing_admins()
|
|
|
|
|
primary = owner_admin = None
|
|
|
|
|
try:
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
primary = _make_user_at("Admin", base)
|
|
|
|
|
owner_admin = _make_user_at("Admin", base + timedelta(seconds=1))
|
|
|
|
|
project = {"user_uid": owner_admin["uid"], "is_private": 1}
|
|
|
|
|
instance = {"created_by": owner_admin["uid"], "project_uid": "p1"}
|
|
|
|
|
assert can_view_instance(instance, project, primary) is True
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(primary, owner_admin)
|
|
|
|
|
_restore_admins(restore)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_manage_instance_only_owner_or_primary_admin(local_db):
|
|
|
|
|
restore = _demote_existing_admins()
|
|
|
|
|
primary = owner_admin = other_admin = None
|
|
|
|
|
try:
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
primary = _make_user_at("Admin", base)
|
|
|
|
|
owner_admin = _make_user_at("Admin", base + timedelta(seconds=1))
|
|
|
|
|
other_admin = _make_user_at("Admin", base + timedelta(seconds=2))
|
|
|
|
|
project = {"user_uid": owner_admin["uid"], "is_private": 1}
|
|
|
|
|
instance = {"created_by": owner_admin["uid"], "project_uid": "p1"}
|
|
|
|
|
assert can_manage_instance(instance, project, owner_admin) is True
|
|
|
|
|
assert can_manage_instance(instance, project, primary) is True
|
|
|
|
|
assert can_manage_instance(instance, project, other_admin) is False
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(primary, owner_admin, other_admin)
|
|
|
|
|
_restore_admins(restore)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_can_manage_instance_other_admin_denied_even_on_public_project(local_db):
|
|
|
|
|
restore = _demote_existing_admins()
|
|
|
|
|
owner_admin = other_admin = None
|
|
|
|
|
try:
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
owner_admin = _make_user_at("Admin", base)
|
|
|
|
|
other_admin = _make_user_at("Admin", base + timedelta(seconds=1))
|
|
|
|
|
project = {"user_uid": owner_admin["uid"], "is_private": 0}
|
|
|
|
|
instance = {"created_by": owner_admin["uid"], "project_uid": "p1"}
|
|
|
|
|
assert can_view_instance(instance, project, other_admin) is True
|
|
|
|
|
assert can_manage_instance(instance, project, other_admin) is False
|
|
|
|
|
finally:
|
|
|
|
|
_purge_users(owner_admin, other_admin)
|
|
|
|
|
_restore_admins(restore)
|
|
|
|
|
|
|
|
|
|
|
2026-06-13 16:32:33 +02:00
|
|
|
def test_canonical_redirect_when_slug_differs():
|
|
|
|
|
item = {"slug": "abcd1234-title", "uid": "abcd1234"}
|
|
|
|
|
assert canonical_redirect("posts", item, "abcd1234-title") is None
|
|
|
|
|
response = canonical_redirect("posts", item, "abcd1234")
|
|
|
|
|
assert response is not None
|
|
|
|
|
assert response.status_code == 301
|
|
|
|
|
assert response.headers["location"] == "/posts/abcd1234-title"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_first_image_url_prefers_inline():
|
|
|
|
|
assert first_image_url({"image": "pic.png"}, None) == "/static/uploads/pic.png"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_first_image_url_uses_first_image_attachment():
|
|
|
|
|
item = {"image": None}
|
|
|
|
|
attachments = [{"is_image": False, "url": "/a"}, {"is_image": True, "url": "/b"}]
|
|
|
|
|
assert first_image_url(item, attachments) == "/b"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_first_image_url_none_when_absent():
|
|
|
|
|
assert first_image_url({}, []) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_enrich_items_attaches_author_and_extras(local_db):
|
|
|
|
|
uid = generate_uid()
|
|
|
|
|
username = f"enrich_{uid[:8]}"
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"username": username,
|
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",
|
2026-06-13 16:32:33 +02:00
|
|
|
"email": f"{username}@t.dev",
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
item_uid = generate_uid()
|
|
|
|
|
items = [
|
|
|
|
|
{
|
|
|
|
|
"uid": item_uid,
|
|
|
|
|
"user_uid": uid,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
authors = get_users_by_uids([uid])
|
|
|
|
|
enriched = enrich_items(
|
|
|
|
|
items,
|
|
|
|
|
"post",
|
|
|
|
|
authors,
|
|
|
|
|
extra_maps={"comment_count": {item_uid: 4}, "flag": lambda item: "ok"},
|
|
|
|
|
)
|
|
|
|
|
entry = enriched[0]
|
|
|
|
|
assert entry["post"] is items[0]
|
|
|
|
|
assert entry["author"]["username"] == username
|
|
|
|
|
assert entry["time_ago"] == "just now"
|
|
|
|
|
assert entry["my_vote"] == 0
|
|
|
|
|
assert entry["comment_count"] == 4
|
|
|
|
|
assert entry["flag"] == "ok"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_readonly_action_requires_confirmation():
|
|
|
|
|
assert confirmation_error("project_set_readonly", {}) is not None
|
|
|
|
|
assert confirmation_error("project_set_readonly", {"confirm": "false"}) is not None
|
|
|
|
|
assert confirmation_error("project_set_readonly", {"confirm": "true"}) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_private_action_requires_confirmation():
|
|
|
|
|
assert confirmation_error("project_set_private", {"value": "true"}) is not None
|
|
|
|
|
assert confirmation_error("project_set_private", {"value": "true", "confirm": "true"}) is None
|
|
|
|
|
assert confirmation_error("vote", {}) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_confirmed_accepts_common_truthy():
|
|
|
|
|
assert _is_confirmed({"confirm": True})
|
|
|
|
|
assert _is_confirmed({"confirm": "yes"})
|
|
|
|
|
assert not _is_confirmed({"confirm": ""})
|
|
|
|
|
assert not _is_confirmed({})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_actions_require_confirmation():
|
|
|
|
|
assert confirmation_error("project_delete_file", {"path": "a.py"}) is not None
|
|
|
|
|
assert confirmation_error("project_delete_file", {"path": "a.py", "confirm": "true"}) is None
|
|
|
|
|
assert confirmation_error("delete_project", {"project_slug": "p"}) is not None
|
|
|
|
|
assert confirmation_error("delete_project", {"project_slug": "p", "confirm": "true"}) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_container_delete_requires_confirmation_but_not_other_actions():
|
|
|
|
|
assert confirmation_error("container_instance_action", {"action": "delete"}) is not None
|
|
|
|
|
assert confirmation_error("container_instance_action", {"action": "restart"}) is None
|
|
|
|
|
assert confirmation_error("container_instance_action", {"action": "start"}) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_destructive_container_exec_requires_confirmation():
|
|
|
|
|
assert confirmation_error("container_exec", {"command": "rm -f /app/cms/cms.db"}) is not None
|
|
|
|
|
assert confirmation_error("container_exec", {"command": "sudo rm -rf /app"}) is not None
|
|
|
|
|
assert confirmation_error("container_exec", {"command": "find . -delete"}) is not None
|
|
|
|
|
assert confirmation_error("container_exec", {"command": "echo x > /etc/hosts"}) is not None
|
|
|
|
|
assert (
|
|
|
|
|
confirmation_error("container_exec", {"command": "rm -rf /app", "confirm": "true"})
|
|
|
|
|
is None
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_benign_container_exec_runs_without_confirmation():
|
|
|
|
|
assert confirmation_error("container_exec", {"command": "pip install requests"}) is None
|
|
|
|
|
assert confirmation_error("container_exec", {"command": "ls -la /app"}) is None
|
|
|
|
|
assert confirmation_error("container_exec", {"command": "grep -rm 5 foo ."}) is None
|