242 lines
8.0 KiB
Python
242 lines
8.0 KiB
Python
|
|
# 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 "")
|