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.
This commit is contained in:
2026-08-09 00:18:20 +02:00
parent 68c2bbe387
commit 8e9d3fad98
348 changed files with 10633 additions and 238 deletions
+4
View File
@@ -132,6 +132,10 @@ Two whole-suite flakes came from tests that pinned an exact number the productio
The rule generalizes: if the production function reads a random-ish input or a table-wide aggregate, assert against **that same function's output for the observed state**, never against the bare constant.
## Signup now has two more required fields
`SignupForm` requires `birth_date` and `accept_terms`. Every test that registers a user must send both - `{"birth_date": "1990-01-01", "accept_terms": "1"}` for an HTTP payload, `page.fill("#birth_date", ...)` plus `page.check("#accept_terms")` for a browser flow. A test that creates a user by inserting straight into `users` must also set `"terms_version": "1"`, or the terms gate will 403 every mutating request that account makes (it is indistinguishable from a real account that has never accepted). `tests/conftest.py` already does all three for the seeded fixtures.
## Common pitfalls
| Pitfall | Fix |
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -29,6 +29,8 @@ def _member_key():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -27,6 +27,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -35,6 +35,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -25,6 +25,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -19,6 +19,8 @@ def _signup_search():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -25,6 +25,8 @@ def _member_dt():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -38,6 +38,8 @@ def member_key():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -21,6 +21,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -21,6 +21,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+241
View File
@@ -0,0 +1,241 @@
# retoor <retoor@molodetz.nl>
import time
import pytest
import requests
from devplacepy.database import get_table, refresh_snapshot, set_setting
from devplacepy.schemas import AdminUserOut
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
_counter = [0]
def _unique(prefix="mod"):
_counter[0] += 1
return f"{prefix}{int(time.time() * 1000)}{_counter[0]}"
def _member(prefix="mod"):
name = _unique(prefix)
session = requests.Session()
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
return session, name
def _admin_session(app_server):
session = requests.Session()
session.post(
f"{BASE_URL}/auth/login",
data={"email": "alice@test.devplace", "password": "secret123"},
allow_redirects=True,
)
return session
def _post(session, content="A post that will be reported by another member."):
response = session.post(
f"{BASE_URL}/posts/create",
data={"title": "Queue subject", "content": content},
headers=JSON,
)
assert response.status_code == 200, response.text
return response.json()["data"]["uid"]
def _report(target_uid, reason="harassment"):
reporter, name = _member("rep")
response = reporter.post(
f"{BASE_URL}/reports/post/{target_uid}",
data={"reason": reason, "detail": "Queue test"},
headers=JSON,
)
assert response.status_code == 200, response.text
refresh_snapshot()
row = get_table("content_reports").find_one(
target_uid=target_uid, deleted_at=None, order_by=["-created_at"]
)
return row["uid"], reporter, name
@pytest.fixture(scope="module")
def admin(app_server, seeded_db):
set_setting("moderation_sla_hours", "24")
return _admin_session(app_server)
def test_queue_is_admin_only(admin, app_server):
member, _ = _member()
assert member.get(f"{BASE_URL}/admin/moderation", headers=JSON).status_code == 403
assert requests.get(f"{BASE_URL}/admin/moderation", headers=JSON).status_code == 401
assert admin.get(f"{BASE_URL}/admin/moderation", headers=JSON).status_code == 200
def test_queue_reports_the_sla_snapshot(admin):
payload = admin.get(f"{BASE_URL}/admin/moderation", headers=JSON).json()
assert payload["sla"]["sla_hours"] == 24
assert "oldest_open_hours" in payload["sla"]
assert set(payload["counts"]) == {"open", "acknowledged", "actioned", "dismissed"}
def test_queue_is_ordered_oldest_open_first(admin):
author, _ = _member("auth")
first = _report(_post(author))[0]
second = _report(_post(author))[0]
payload = admin.get(f"{BASE_URL}/admin/moderation?status=open", headers=JSON).json()
order = [entry["uid"] for entry in payload["reports"]]
assert order.index(first) < order.index(second)
def test_the_report_detail_projects_the_subject_through_the_admin_user_model(admin):
author, author_name = _member("subj")
report_uid = _report(_post(author))[0]
payload = admin.get(
f"{BASE_URL}/admin/moderation/{report_uid}", headers=JSON
).json()
subject = payload["subject"]
assert subject["username"] == author_name
assert "password_hash" not in subject
assert "api_key" not in subject
assert set(subject) <= set(AdminUserOut.model_fields)
def test_acknowledging_keeps_the_report_open_for_a_decision(admin):
author, _ = _member("auth")
report_uid = _report(_post(author))[0]
response = admin.post(
f"{BASE_URL}/admin/moderation/{report_uid}/status",
data={"status": "acknowledged"},
headers=JSON,
)
assert response.status_code == 200
refresh_snapshot()
assert (
get_table("content_reports").find_one(uid=report_uid)["status"]
== "acknowledged"
)
def test_a_decision_records_an_action_row_and_resolves_the_report(admin):
author, _ = _member("auth")
post_uid = _post(author)
report_uid, _, _ = _report(post_uid)
response = admin.post(
f"{BASE_URL}/admin/moderation/{report_uid}/decide",
data={"action": "remove_content", "reason": "Breaks the guidelines"},
headers=JSON,
)
assert response.status_code == 200, response.text
refresh_snapshot()
report = get_table("content_reports").find_one(uid=report_uid)
assert report["status"] == "actioned"
assert report["resolved_at"]
action = get_table("moderation_actions").find_one(report_uid=report_uid)
assert action["action"] == "remove_content"
assert action["reason"] == "Breaks the guidelines"
assert get_table("posts").find_one(uid=post_uid)["deleted_at"]
def test_deciding_a_resolved_report_twice_is_refused(admin):
author, _ = _member("auth")
report_uid = _report(_post(author))[0]
first = admin.post(
f"{BASE_URL}/admin/moderation/{report_uid}/decide",
data={"action": "dismiss"},
headers=JSON,
)
assert first.status_code == 200
second = admin.post(
f"{BASE_URL}/admin/moderation/{report_uid}/decide",
data={"action": "dismiss"},
headers=JSON,
)
assert second.status_code == 409
def test_escalating_raises_severity_and_keeps_the_report_open(admin):
author, _ = _member("auth")
report_uid = _report(_post(author), reason="spam")[0]
response = admin.post(
f"{BASE_URL}/admin/moderation/{report_uid}/decide",
data={"action": "escalate"},
headers=JSON,
)
assert response.status_code == 200
refresh_snapshot()
report = get_table("content_reports").find_one(uid=report_uid)
assert report["severity"] == "critical"
assert report["status"] == "acknowledged"
def test_suspension_blocks_writing_but_not_reading_or_deleting(admin):
author, author_name = _member("susp")
refresh_snapshot()
subject = get_table("users").find_one(username=author_name)
response = admin.post(
f"{BASE_URL}/admin/users/{subject['uid']}/suspend",
data={"reason": "Testing enforcement", "duration_hours": "24"},
headers=JSON,
)
assert response.status_code == 200, response.text
try:
blocked = author.post(
f"{BASE_URL}/posts/create",
data={"title": "nope", "content": "This must not be published at all."},
headers=JSON,
)
assert blocked.status_code == 403
assert author.get(f"{BASE_URL}/feed", headers=JSON).status_code == 200
assert (
author.get(f"{BASE_URL}/profile/{author_name}/delete", headers=JSON).status_code
== 200
)
finally:
admin.post(f"{BASE_URL}/admin/users/{subject['uid']}/lift", headers=JSON)
allowed = author.post(
f"{BASE_URL}/posts/create",
data={"title": "back", "content": "Writing works again after the lift."},
headers=JSON,
)
assert allowed.status_code == 200
def test_ban_revokes_every_session(admin):
victim, victim_name = _member("ban")
refresh_snapshot()
subject = get_table("users").find_one(username=victim_name)
response = admin.post(
f"{BASE_URL}/admin/users/{subject['uid']}/ban",
data={"reason": "Testing the ban path"},
headers=JSON,
)
assert response.status_code == 200
refresh_snapshot()
assert get_table("users").find_one(uid=subject["uid"])["is_active"] in (0, False)
assert victim.get(f"{BASE_URL}/reports/mine", headers=JSON).status_code == 401
def test_an_admin_cannot_enforce_against_themselves(admin, seeded_db):
refresh_snapshot()
alice = get_table("users").find_one(username="alice_test")
response = admin.post(
f"{BASE_URL}/admin/users/{alice['uid']}/suspend",
data={"reason": "self", "duration_hours": "1"},
headers=JSON,
)
assert response.status_code == 200
refresh_snapshot()
assert not (get_table("users").find_one(uid=alice["uid"])["suspended_until"] or "")
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -41,6 +41,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -23,6 +23,8 @@ def _member_trash():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -23,6 +23,8 @@ def _member_trash():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -23,6 +23,8 @@ def _member_trash():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -17,6 +17,8 @@ def _session_content_negotiation(password="secret123"):
"email": f"{name}@t.dev",
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -44,6 +44,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -18,6 +18,8 @@ def _signup_api_auth(password="secret123"):
"email": email,
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -24,6 +24,8 @@ def _devrant_user():
"email": f"{name}@t.dev",
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+4
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -128,6 +130,8 @@ def test_login_logout_recorded(seeded_db):
"email": email,
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -27,6 +27,8 @@ def _signup_auth_matrix(role="Member"):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+4
View File
@@ -13,6 +13,8 @@ def _session_validation():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -27,6 +29,8 @@ def test_signup_short_username_rerenders_with_message(app_server):
"email": "x@y.zz",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=False,
)
+154
View File
@@ -0,0 +1,154 @@
# retoor <retoor@molodetz.nl>
import time
import requests
from devplacepy.database import get_setting, get_table, refresh_snapshot, set_setting
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
_counter = [0]
def _unique(prefix="terms"):
_counter[0] += 1
return f"{prefix}{int(time.time() * 1000)}{_counter[0]}"
def _signup(**overrides):
name = _unique()
payload = {
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
}
payload.update(overrides)
session = requests.Session()
response = session.post(
f"{BASE_URL}/auth/signup", data=payload, headers=JSON, allow_redirects=False
)
return session, payload["username"], response
def test_signup_without_accepting_the_terms_is_refused(app_server):
_, name, response = _signup(accept_terms="")
assert response.status_code == 422
assert "accept" in response.text.lower()
refresh_snapshot()
assert get_table("users").find_one(username=name) is None
def test_signup_below_the_minimum_age_is_refused(app_server):
from datetime import date
young = date.today().replace(year=date.today().year - 12).isoformat()
_, name, response = _signup(birth_date=young)
assert response.status_code == 422
assert "years old" in response.text
refresh_snapshot()
assert get_table("users").find_one(username=name) is None
def test_signup_stores_the_band_and_the_acceptance_but_not_the_date(app_server):
_, name, response = _signup(birth_date="1990-06-15")
assert response.status_code == 200, response.text
refresh_snapshot()
row = get_table("users").find_one(username=name)
assert row["age_band"] == "adult"
assert row["terms_version"] == (get_setting("terms_version", "1") or "1")
assert row["terms_accepted_at"]
assert "1990-06-15" not in str(dict(row))
consents = {
entry["kind"]
for entry in get_table("user_consents").find(
owner_kind="user", owner_id=row["uid"], state="granted", deleted_at=None
)
}
assert {"terms", "privacy", "activity_recording"} <= consents
assert "ai_third_party" not in consents
def test_the_acceptance_page_reports_both_versions(app_server):
session, name, response = _signup()
assert response.status_code == 200
page = session.get(f"{BASE_URL}/auth/accept-terms", headers=JSON)
assert page.status_code == 200, page.text
body = page.json()
in_force = get_setting("terms_version", "1") or "1"
assert body["terms_version"] == in_force
assert body["accepted_version"] == in_force
assert "consents" not in body
refresh_snapshot()
assert get_table("users").find_one(username=name)["terms_version"] == in_force
def test_the_acceptance_page_is_signed_in_only(app_server):
assert (
requests.get(f"{BASE_URL}/auth/accept-terms", headers=JSON).status_code == 401
)
def test_the_acceptance_page_shows_the_gap_after_a_bump(app_server):
session, _, response = _signup()
assert response.status_code == 200
original = get_setting("terms_version", "1") or "1"
set_setting("terms_version", f"{original}-gap")
try:
body = {}
for _ in range(40):
body = session.get(f"{BASE_URL}/auth/accept-terms", headers=JSON).json()
if body.get("terms_version") == f"{original}-gap":
break
time.sleep(0.5)
assert body["terms_version"] == f"{original}-gap"
assert body["accepted_version"] == original
finally:
set_setting("terms_version", original)
def test_bumping_the_version_gates_writes_but_never_reads(app_server):
session, name, response = _signup()
assert response.status_code == 200
original = get_setting("terms_version", "1") or "1"
set_setting("terms_version", f"{original}-bumped")
try:
blocked = None
for _ in range(40):
blocked = session.post(
f"{BASE_URL}/posts/create",
data={
"title": "gated",
"content": "This write must be gated by the terms version.",
},
headers=JSON,
)
if blocked.status_code == 403:
break
time.sleep(0.5)
assert blocked.status_code == 403
assert session.get(f"{BASE_URL}/feed", headers=JSON).status_code == 200
assert session.get(f"{BASE_URL}/docs/terms.html").status_code == 200
assert (
session.get(f"{BASE_URL}/profile/{name}/delete", headers=JSON).status_code
== 200
)
accepted = session.post(f"{BASE_URL}/auth/accept-terms", headers=JSON)
assert accepted.status_code == 200
refresh_snapshot()
assert (
get_table("users").find_one(username=name)["terms_version"]
== f"{original}-bumped"
)
allowed = session.post(
f"{BASE_URL}/posts/create",
data={"title": "ok", "content": "Writing works again after acceptance."},
headers=JSON,
)
assert allowed.status_code == 200
finally:
set_setting("terms_version", original)
+2
View File
@@ -33,6 +33,8 @@ def _signup(password="secret123"):
"email": email,
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+1
View File
@@ -47,6 +47,7 @@ def _user():
{
"uid": uid,
"username": f"aw_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@t.dev",
"created_at": datetime.now(timezone.utc).isoformat(),
}
+2
View File
@@ -20,6 +20,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -19,6 +19,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -23,6 +23,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+4
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -130,6 +132,8 @@ def _session_bookmarks():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -19,6 +19,8 @@ def _session_bookmarks():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+6
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -129,6 +131,8 @@ def _session_comments():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -159,6 +163,8 @@ def _user_uploads(prefix="up"):
"email": f"{name}@test.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -20,6 +20,8 @@ def _session_edit():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+1
View File
@@ -30,6 +30,7 @@ def _owner():
{
"uid": uid,
"username": f"dpt_{uid[-6:]}",
"terms_version": "1",
"email": f"{uid[-6:]}@title.test",
"password_hash": "x",
"role": "Member",
+2
View File
@@ -17,6 +17,8 @@ def _session_content_negotiation(password="secret123"):
"email": f"{name}@t.dev",
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -35,6 +35,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -19,6 +19,7 @@ def _seed_schema():
{
"uid": generate_uid(),
"username": "dbapi_seed",
"terms_version": "1",
"email": "seed@dbapi.test",
"role": "Member",
"xp": 0,
@@ -66,6 +67,7 @@ def auth(client):
{
"uid": uid,
"username": f"dbapi_admin_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@dbapi.test",
"role": "Admin",
"api_key": f"dbapikey_{uid}",
+9 -3
View File
@@ -196,16 +196,22 @@ def test_edit_profile_updates_bio(app_server):
assert get_table("users").find_one(username=name)["bio"] == "devrant bio text"
def test_delete_account_deactivates_and_revokes(app_server):
def test_delete_account_deletes_and_revokes(app_server):
name, email, password, _ = _register()
refresh_snapshot()
uid = get_table("users").find_one(username=name)["uid"]
params = _token_params(name, password)
r = requests.delete(f"{BASE_URL}/api/users/me", data=params)
assert r.json()["success"] is True
refresh_snapshot()
user = get_table("users").find_one(username=name)
assert get_table("users").find_one(username=name) is None
user = get_table("users").find_one(uid=uid)
assert user["is_active"] in (False, 0)
assert user["username"].startswith("deleted_")
assert not user["email"]
assert not user["password_hash"]
assert (
get_table("devrant_tokens").count(user_uid=user["uid"], deleted_at=None) == 0
get_table("devrant_tokens").count(user_uid=uid, deleted_at=None) == 0
)
+4
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -145,6 +147,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+9
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -128,6 +130,8 @@ def _session_polls():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -197,6 +201,7 @@ def _seed_owner():
{
"uid": uid,
"username": f"seo_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@seo.test",
"password_hash": "x",
"role": "Member",
@@ -371,6 +376,8 @@ def test_feed_shows_online_now_section(app_server):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -391,6 +398,8 @@ def test_online_now_avatar_uses_the_shared_presence_dot(app_server):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+6
View File
@@ -18,6 +18,8 @@ def _signup_api_auth(password="secret123"):
"email": email,
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -73,6 +75,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -150,6 +154,8 @@ def _session_follow_api():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -16,6 +16,8 @@ def _session_follow_api():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -23,6 +23,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -24,6 +24,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+8
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -137,6 +139,8 @@ def _register_session(prefix):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -147,6 +151,7 @@ def _seed_gists(count):
{
"uid": owner,
"username": f"pag_{owner[:8]}",
"terms_version": "1",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
@@ -212,6 +217,7 @@ def _seed_searchable_gists(language="python"):
{
"uid": owner,
"username": f"gsrch_{owner[:8]}",
"terms_version": "1",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
@@ -254,6 +260,8 @@ def _user_uploads(prefix="up"):
"email": f"{name}@test.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+1
View File
@@ -38,6 +38,7 @@ def _seed_owner():
{
"uid": uid,
"username": f"seo_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@seo.test",
"password_hash": "x",
"role": "Member",
+1
View File
@@ -64,6 +64,7 @@ def _user(role="member"):
{
"uid": uid,
"username": f"iatt{_seq[0]}",
"terms_version": "1",
"role": role,
"xp": 0,
"level": 1,
+3
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -163,6 +165,7 @@ def _make_user_issues_gitea():
{
"uid": uid,
"username": username,
"terms_version": "1",
"role": "member",
"xp": 0,
"level": 1,
+2
View File
@@ -50,6 +50,7 @@ def admin_headers(client):
{
"uid": uid,
"username": f"gitea_admin_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@gitea.test",
"role": "Admin",
"api_key": f"giteakey_{uid}",
@@ -80,6 +81,7 @@ def member_headers(client):
{
"uid": uid,
"username": f"gitea_member_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@giteamember.test",
"role": "Member",
"api_key": key,
+2
View File
@@ -35,6 +35,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -13,6 +13,8 @@ def _session_push():
"email": f"{name}@test.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -21,6 +21,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -21,6 +21,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -21,6 +21,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -24,6 +24,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+6
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -129,6 +131,8 @@ def _session_content_negotiation(password="secret123"):
"email": f"{name}@t.dev",
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -194,6 +198,8 @@ def test_chat_presence_agrees_with_the_feed_online_roster(app_server):
"email": f"{other_name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -16,6 +16,8 @@ def _user_uploads(prefix="up"):
"email": f"{name}@test.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -52,6 +52,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -47,6 +47,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -43,6 +43,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -21,6 +21,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -20,6 +20,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+1
View File
@@ -38,6 +38,7 @@ def _seed_owner():
{
"uid": uid,
"username": f"seo_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@seo.test",
"password_hash": "x",
"role": "Member",
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -17,6 +17,8 @@ def _session_polls():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+13
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -129,6 +131,8 @@ def _session_content_negotiation(password="secret123"):
"email": f"{name}@t.dev",
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -147,6 +151,8 @@ def _session_polls():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -199,6 +205,8 @@ def _session_reactions():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -292,6 +300,7 @@ def _seed_owner():
{
"uid": uid,
"username": f"seo_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@seo.test",
"password_hash": "x",
"role": "Member",
@@ -431,6 +440,8 @@ def _user_uploads(prefix="up"):
"email": f"{name}@test.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -458,6 +469,8 @@ def _session_validation():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+4
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -129,6 +131,8 @@ def _session_polls():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -17,6 +17,8 @@ def _session_content_negotiation(password="secret123"):
"email": f"{name}@t.dev",
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -27,6 +27,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -27,6 +27,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -20,6 +20,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+210
View File
@@ -0,0 +1,210 @@
# retoor <retoor@molodetz.nl>
import time
import requests
from devplacepy.database import (
CONSENT_KINDS,
consent_granted,
get_table,
refresh_snapshot,
)
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
_counter = [0]
def _unique(prefix="cons"):
_counter[0] += 1
return f"{prefix}{int(time.time() * 1000)}{_counter[0]}"
def _member():
name = _unique()
session = requests.Session()
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
refresh_snapshot()
return session, name, get_table("users").find_one(username=name)
def test_the_privacy_tab_lists_every_consent(app_server):
session, name, _ = _member()
payload = session.get(
f"{BASE_URL}/profile/{name}?tab=privacy", headers=JSON
).json()
kinds = {entry["kind"] for entry in payload["consents"]}
assert set(CONSENT_KINDS) == kinds
states = {entry["kind"]: entry["state"] for entry in payload["consents"]}
assert states["ai_third_party"] == "withdrawn"
assert states["terms"] == "granted"
def test_granting_and_withdrawing_takes_effect_immediately(app_server):
session, name, user = _member()
granted = session.post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": "1"},
headers=JSON,
)
assert granted.status_code == 200
refresh_snapshot()
assert consent_granted("user", user["uid"], "ai_third_party") is True
withdrawn = session.post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": "0"},
headers=JSON,
)
assert withdrawn.status_code == 200
refresh_snapshot()
assert consent_granted("user", user["uid"], "ai_third_party") is False
def test_consent_history_is_never_rewritten(app_server):
session, name, user = _member()
for granted in ("1", "0", "1"):
session.post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": granted},
headers=JSON,
)
refresh_snapshot()
rows = list(
get_table("user_consents").find(
owner_kind="user", owner_id=user["uid"], kind="ai_third_party"
)
)
assert len(rows) == 3
def test_an_unknown_consent_kind_is_refused(app_server):
session, name, _ = _member()
response = session.post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "not_a_consent", "granted": "1"},
headers=JSON,
)
assert response.status_code == 422
def test_the_gateway_refuses_user_content_without_consent(app_server):
session, name, user = _member()
refresh_snapshot()
api_key = get_table("users").find_one(uid=user["uid"])["api_key"]
headers = {**JSON, "Authorization": f"Bearer {api_key}"}
body = {"model": "molodetz", "messages": [{"role": "user", "content": "hi"}]}
refused = requests.post(
f"{BASE_URL}/openai/v1/chat/completions", json=body, headers=headers
)
assert refused.status_code == 403
assert "consent" in refused.text.lower()
session.post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": "1"},
headers=JSON,
)
allowed = requests.post(
f"{BASE_URL}/openai/v1/chat/completions", json=body, headers=headers
)
assert allowed.status_code != 403
def _admin():
session = requests.Session()
session.post(
f"{BASE_URL}/auth/login",
data={"email": "alice@test.devplace", "password": "secret123"},
allow_redirects=True,
)
return session
def test_an_admin_cannot_grant_a_consent_for_someone_else(app_server, seeded_db):
_, name, user = _member()
response = _admin().post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": "1"},
headers=JSON,
)
assert response.status_code == 403
assert "account holder" in response.text
refresh_snapshot()
assert consent_granted("user", user["uid"], "ai_third_party") is False
def test_an_admin_cannot_withdraw_a_consent_for_someone_else(app_server, seeded_db):
session, name, user = _member()
session.post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": "1"},
headers=JSON,
)
response = _admin().post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": "0"},
headers=JSON,
)
assert response.status_code == 403
refresh_snapshot()
assert consent_granted("user", user["uid"], "ai_third_party") is True
def test_an_admin_cannot_change_someone_elses_mature_preference(app_server, seeded_db):
_, name, user = _member()
before = get_table("users").find_one(uid=user["uid"])["mature_opt_in"]
response = _admin().post(
f"{BASE_URL}/profile/{name}/mature-content",
data={"mature_opt_in": "1"},
headers=JSON,
)
assert response.status_code == 403
assert "account holder" in response.text
refresh_snapshot()
assert get_table("users").find_one(uid=user["uid"])["mature_opt_in"] == before
def test_a_stranger_cannot_change_another_members_consent(app_server):
_, name, user = _member()
stranger, _, _ = _member()
response = stranger.post(
f"{BASE_URL}/profile/{name}/consent",
data={"kind": "ai_third_party", "granted": "1"},
headers=JSON,
)
assert response.status_code == 403
refresh_snapshot()
assert consent_granted("user", user["uid"], "ai_third_party") is False
def test_the_mature_preference_round_trips(app_server):
session, name, user = _member()
response = session.post(
f"{BASE_URL}/profile/{name}/mature-content",
data={"mature_opt_in": "1"},
headers=JSON,
)
assert response.status_code == 200
refresh_snapshot()
assert get_table("users").find_one(uid=user["uid"])["mature_opt_in"] in (1, True)
session.post(
f"{BASE_URL}/profile/{name}/mature-content",
data={"mature_opt_in": "0"},
headers=JSON,
)
refresh_snapshot()
assert get_table("users").find_one(uid=user["uid"])["mature_opt_in"] in (0, False)
+162
View File
@@ -0,0 +1,162 @@
# retoor <retoor@molodetz.nl>
import time
import requests
from devplacepy.database import get_table, refresh_snapshot
from tests.conftest import BASE_URL
JSON = {"Accept": "application/json"}
_counter = [0]
def _unique(prefix="del"):
_counter[0] += 1
return f"{prefix}{int(time.time() * 1000)}{_counter[0]}"
def _member(prefix="del"):
name = _unique(prefix)
session = requests.Session()
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
return session, name
def _post(session, title="Doomed post"):
response = session.post(
f"{BASE_URL}/posts/create",
data={"title": title, "content": "Content that disappears with the account."},
headers=JSON,
)
assert response.status_code == 200, response.text
return response.json()["data"]["uid"]
def test_the_page_states_what_is_removed_and_the_grace_window(app_server):
session, name = _member()
payload = session.get(f"{BASE_URL}/profile/{name}/delete", headers=JSON).json()
assert payload["username"] == name
assert payload["removed"]
assert payload["retained"]
assert payload["grace_hours"] >= 0
def test_a_wrong_password_does_not_delete(app_server):
session, name = _member()
response = session.post(
f"{BASE_URL}/profile/{name}/delete",
data={"password": "not-the-password"},
headers=JSON,
)
assert response.status_code == 403
refresh_snapshot()
assert get_table("users").find_one(username=name) is not None
def test_deletion_removes_content_revokes_sessions_and_anonymises(app_server):
session, name = _member()
post_uid = _post(session)
refresh_snapshot()
before = get_table("users").find_one(username=name)
response = session.post(
f"{BASE_URL}/profile/{name}/delete",
data={"password": "secret123"},
headers=JSON,
)
assert response.status_code == 200, response.text
stamp = response.json()["data"]["stamp"]
refresh_snapshot()
assert get_table("users").find_one(username=name) is None
row = get_table("users").find_one(uid=before["uid"])
assert row["username"].startswith("deleted_")
assert not row["email"]
assert not row["api_key"]
assert not row["password_hash"]
assert row["deletion_requested_at"] == stamp
post = get_table("posts").find_one(uid=post_uid)
assert post["deleted_at"] == stamp
assert session.get(f"{BASE_URL}/reports/mine", headers=JSON).status_code == 401
assert requests.get(f"{BASE_URL}/profile/{name}").status_code == 404
def test_only_the_account_holder_can_delete(app_server, seeded_db):
victim, victim_name = _member()
admin = requests.Session()
admin.post(
f"{BASE_URL}/auth/login",
data={"email": "alice@test.devplace", "password": "secret123"},
allow_redirects=True,
)
response = admin.post(
f"{BASE_URL}/profile/{victim_name}/delete",
data={"password": "secret123"},
headers=JSON,
)
assert response.status_code == 403
refresh_snapshot()
assert get_table("users").find_one(username=victim_name) is not None
def test_restore_from_trash_brings_the_whole_event_back(app_server, seeded_db):
session, name = _member()
post_uid = _post(session, title="Restorable post")
session.post(
f"{BASE_URL}/profile/{name}/delete",
data={"password": "secret123"},
headers=JSON,
)
refresh_snapshot()
assert get_table("posts").find_one(uid=post_uid)["deleted_at"]
admin = requests.Session()
admin.post(
f"{BASE_URL}/auth/login",
data={"email": "alice@test.devplace", "password": "secret123"},
allow_redirects=True,
)
response = admin.post(
f"{BASE_URL}/admin/trash/posts/{post_uid}/restore", headers=JSON
)
assert response.status_code == 200
refresh_snapshot()
assert get_table("posts").find_one(uid=post_uid)["deleted_at"] is None
def test_purging_after_the_grace_window_leaves_no_personal_data(app_server):
from devplacepy.services.moderation import deletion
session, name = _member()
post_uid = _post(session, title="Purged post")
refresh_snapshot()
uid = get_table("users").find_one(username=name)["uid"]
session.post(
f"{BASE_URL}/profile/{name}/delete",
data={"password": "secret123"},
headers=JSON,
)
refresh_snapshot()
assert get_table("users").find_one(uid=uid)["username"].startswith("deleted_")
from datetime import datetime, timedelta, timezone
later = datetime.now(timezone.utc) + timedelta(hours=deletion.grace_hours() + 1)
purged = deletion.purge_due(now=later)
assert purged >= 1
refresh_snapshot()
assert get_table("posts").find_one(uid=post_uid) is None
assert get_table("users").find_one(uid=uid) is None
+123
View File
@@ -38,6 +38,7 @@ def _seed_owner():
{
"uid": uid,
"username": f"seo_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@seo.test",
"password_hash": "x",
"role": "Member",
@@ -278,6 +279,8 @@ def test_profile_json_exposes_online_presence(app_server):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -326,6 +329,8 @@ def test_profile_hero_avatar_has_presence_dot(app_server):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -354,6 +359,8 @@ def test_followers_list_avatar_has_presence_dot(app_server):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -381,6 +388,8 @@ def test_viewing_profile_marks_notification_read(app_server):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -511,6 +520,8 @@ def _signup_badge_user():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -571,4 +582,116 @@ def test_profile_badge_description_exact_string(app_server):
)
PRIVACY_FIELDS = (
"age_band",
"terms_version",
"terms_accepted_at",
"suspended_until",
"suspension_reason",
)
def _privacy_member(prefix="priv"):
import time
name = f"{prefix}{int(time.time() * 1000000)}"
session = requests.Session()
session.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
return session, name
def _admin_session():
session = requests.Session()
session.post(
f"{BASE_URL}/auth/login",
data={"email": "alice@test.devplace", "password": "secret123"},
allow_redirects=True,
)
return session
def test_the_owner_reads_their_own_privacy_state(app_server):
session, name = _privacy_member()
body = session.get(
f"{BASE_URL}/profile/{name}?tab=privacy",
headers={"Accept": "application/json"},
).json()
assert body["age_band"] == "adult"
assert body["terms_version"]
assert body["terms_accepted_at"]
assert body["consents"]
def test_privacy_state_is_withheld_from_a_stranger(app_server):
_, name = _privacy_member()
stranger, _ = _privacy_member("prvs")
body = stranger.get(
f"{BASE_URL}/profile/{name}?tab=privacy",
headers={"Accept": "application/json"},
).json()
for field in PRIVACY_FIELDS:
assert body[field] in ("", None), f"{field} leaked to a stranger: {body[field]!r}"
assert body["mature_opt_in"] is False
assert body["consents"] == []
def test_privacy_state_is_withheld_from_a_guest(app_server):
_, name = _privacy_member()
body = requests.get(
f"{BASE_URL}/profile/{name}?tab=privacy",
headers={"Accept": "application/json"},
).json()
for field in PRIVACY_FIELDS:
assert body[field] in ("", None), f"{field} leaked to a guest: {body[field]!r}"
assert body["consents"] == []
def test_a_strangers_profile_html_never_renders_the_privacy_panel(app_server):
_, name = _privacy_member()
stranger, _ = _privacy_member("prvh")
html = stranger.get(f"{BASE_URL}/profile/{name}?tab=privacy").text
assert "privacy-panel" not in html
assert f"/profile/{name}/consent" not in html
assert f"/profile/{name}/delete" not in html
def test_an_admin_reads_the_state_but_gets_no_controls(app_server, seeded_db):
_, name = _privacy_member()
admin = _admin_session()
body = admin.get(
f"{BASE_URL}/profile/{name}?tab=privacy",
headers={"Accept": "application/json"},
).json()
assert body["age_band"] == "adult"
assert body["consents"]
html = admin.get(f"{BASE_URL}/profile/{name}?tab=privacy").text
assert "privacy-panel" in html
assert "Only the account holder can change this." in html
assert "Only the account holder can change this preference." in html
assert f'action="/profile/{name}/consent"' not in html
assert f'action="/profile/{name}/mature-content"' not in html
assert f"/profile/{name}/delete" not in html
def test_the_owner_gets_the_privacy_controls(app_server):
session, name = _privacy_member()
html = session.get(f"{BASE_URL}/profile/{name}?tab=privacy").text
assert f'action="/profile/{name}/consent"' in html
assert f'action="/profile/{name}/mature-content"' in html
assert f"/profile/{name}/delete" in html
assert "Only the account holder can change this." not in html
+2
View File
@@ -28,6 +28,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -18,6 +18,8 @@ def _signup_api_auth(password="secret123"):
"email": email,
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -27,6 +27,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+9
View File
@@ -18,6 +18,8 @@ def _signup_api_auth(password="secret123"):
"email": email,
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -44,6 +46,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -143,6 +147,7 @@ def _make_user_streaks():
{
"uid": uid,
"username": name,
"terms_version": "1",
"email": f"{name}@t.dev",
"role": "Member",
"is_active": True,
@@ -256,6 +261,8 @@ def test_profile_renders_heatmap_and_streak(app_server):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -287,6 +294,8 @@ def test_profile_badges_json_has_non_null_names(app_server):
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -21,6 +21,8 @@ def _signup(password="secret123"):
"email": f"{name}@t.dev",
"password": password,
"confirm_password": password,
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+4
View File
@@ -50,6 +50,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -125,6 +127,8 @@ def _session_validation():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -17,6 +17,8 @@ def _signup_project_files():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -24,6 +24,8 @@ def _signup_project_visibility():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -19,6 +19,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -25,6 +25,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -16,6 +16,8 @@ def _user_uploads(prefix="up"):
"email": f"{name}@test.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+4
View File
@@ -17,6 +17,8 @@ def _signup_project_files():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -132,6 +134,8 @@ def _signup_project_visibility():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -40,6 +40,8 @@ def _member():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -24,6 +24,8 @@ def _signup_project_visibility():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -27,6 +27,8 @@ def _signup():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -28,6 +28,8 @@ def _signup_project_file_lines():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)

Some files were not shown because too many files have changed in this diff Show More