132 lines
3.7 KiB
Python
Raw Normal View History

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
# retoor <retoor@molodetz.nl>
from datetime import date
import pytest
from devplacepy.database import (
AGE_BANDS,
MATURITY_TARGETS,
REPORTABLE_TARGETS,
REPORT_REASONS,
SOFT_DELETE_TABLES,
UNREPORTABLE_TABLES,
age_band_for,
years_between,
)
from devplacepy.database.moderation import MODERATION_TABLES
GUIDELINE_REASONS = (
"hate",
"violence",
"weapons",
"sexual",
"religious",
"misinformation",
"exploitative",
)
def test_every_visible_soft_delete_table_is_reportable():
reportable_tables = set(REPORTABLE_TARGETS.values())
excluded = set(UNREPORTABLE_TABLES) | set(MODERATION_TABLES)
unclassified = [
table
for table in SOFT_DELETE_TABLES
if table not in reportable_tables and table not in excluded
]
assert unclassified == []
def test_exclusion_list_only_names_real_soft_delete_tables():
unknown = [table for table in UNREPORTABLE_TABLES if table not in SOFT_DELETE_TABLES]
assert unknown == []
def test_registry_tables_are_unique_and_maturity_is_a_subset():
assert len(set(REPORTABLE_TARGETS.values())) == len(REPORTABLE_TARGETS)
assert MATURITY_TARGETS <= set(REPORTABLE_TARGETS)
def test_report_reasons_cover_every_guideline_category():
for reason in GUIDELINE_REASONS:
assert reason in REPORT_REASONS
assert REPORT_REASONS["intellectual_property"]
assert REPORT_REASONS["other"]
def test_every_registry_target_resolves_to_a_url(local_db):
from devplacepy.database import resolve_object_url
for target_type in REPORTABLE_TARGETS:
url = resolve_object_url(target_type, "missing-uid")
assert url.startswith("/")
def test_resolve_object_url_points_at_the_owning_surface(local_db):
from devplacepy.database import get_table, resolve_object_url
get_table("users").insert(
{"uid": "mod-user-1", "username": "mod_user_1", "deleted_at": None}
)
get_table("messages").insert(
{
"uid": "mod-msg-1",
"sender_uid": "mod-user-1",
"receiver_uid": "mod-user-2",
"content": "hello",
}
)
try:
assert resolve_object_url("user", "mod-user-1") == "/profile/mod_user_1"
assert (
resolve_object_url("message", "mod-msg-1")
== "/messages?with_uid=mod-user-1"
)
assert resolve_object_url("devii_output", "anything") == "/devii"
finally:
get_table("users").delete(uid="mod-user-1")
get_table("messages").delete(uid="mod-msg-1")
@pytest.mark.parametrize(
"age,expected",
[
(0, "under_min"),
(12, "under_min"),
(13, "13_15"),
(15, "13_15"),
(16, "16_17"),
(17, "16_17"),
(18, "adult"),
(120, "adult"),
],
)
def test_age_band_boundaries(age, expected):
assert age_band_for(age) == expected
def test_age_band_is_monotonic_across_the_whole_domain():
order = {band: index for index, band in enumerate(AGE_BANDS)}
previous = -1
for age in range(0, 150):
current = order[age_band_for(age)]
assert current >= previous
previous = current
def test_years_between_handles_leap_days_and_exact_boundaries():
assert years_between(date(2000, 2, 29), date(2018, 2, 28)) == 17
assert years_between(date(2000, 2, 29), date(2018, 3, 1)) == 18
assert years_between(date(2008, 1, 1), date(2026, 1, 1)) == 18
assert years_between(date(2008, 1, 2), date(2026, 1, 1)) == 17
def test_years_between_never_exceeds_the_calendar_difference():
born = date(2005, 6, 15)
for offset in range(0, 4000, 7):
today = date.fromordinal(born.toordinal() + offset)
years = years_between(born, today)
assert 0 <= years <= today.year - born.year