diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_moderation.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_moderation.py | 205 |
1 files changed, 163 insertions, 42 deletions
diff --git a/packages/meshbay-hub/tests/test_moderation.py b/packages/meshbay-hub/tests/test_moderation.py index b328d67..b599c84 100644 --- a/packages/meshbay-hub/tests/test_moderation.py +++ b/packages/meshbay-hub/tests/test_moderation.py @@ -48,77 +48,198 @@ async def admin_headers(client): return headers +async def _policy(client, admin, **values): + r = await client.patch("/v1/admin/settings", json={"reports": values}, headers=admin) + assert r.status_code == 200, r.text + return r.json()["reports"] + + +async def _public_group(client, db_session, owner, name="commons-mod"): + from datetime import UTC, datetime + + from meshbay_hub.db.models import Group + r = await client.post("/v1/groups", json={"name": name, "visibility": "public", + "join_policy": "open"}, headers=owner) + assert r.status_code == 201, r.text + gid = r.json()["group_id"] + (await db_session.get(Group, gid)).hosted_at = datetime.now(UTC) + await db_session.commit() + return gid + + +async def _members(client, gid, names): + out = [] + for n in names: + h = await _register_and_login(client, n) + assert (await client.post(f"/v1/groups/{gid}/join", headers=h)).status_code == 200 + out.append(h) + return out + + +async def _report(client, headers, gid, h=FAKE_HASH, **extra): + return await client.post("/v1/reports", headers=headers, + json={"content_hash": h, "group_id": gid, + "reason": "illegal", **extra}) + + +@pytest.fixture +async def setting(client, admin_headers): + """Reports allowed from a new account, so tests need not wait a day.""" + await _policy(client, admin_headers, min_account_age_hours=0) + return admin_headers + + @pytest.mark.asyncio async def test_report_requires_auth(client): - # No credentials at all — FastAPI rejects the missing header before the body. r = await client.post("/v1/reports", json={ - "content_hash": FAKE_HASH, "reason": "illegal"}) + "content_hash": FAKE_HASH, "group_id": "g", "reason": "illegal"}) assert r.status_code in (401, 422) - - # A bogus token is a clean 401. r = await client.post("/v1/reports", - json={"content_hash": FAKE_HASH, "reason": "illegal"}, + json={"content_hash": FAKE_HASH, "group_id": "g", + "reason": "illegal"}, headers={"Authorization": "Bearer not-a-real-token"}) assert r.status_code == 401 @pytest.mark.asyncio -async def test_report_content_logged(client, reporter): - r = await client.post("/v1/reports", - json={"content_hash": FAKE_HASH, "reason": "illegal"}, - headers=reporter) - assert r.status_code == 201 - data = r.json() - assert data["report_count"] == 1 - assert data["status"] == "logged" +async def test_a_node_token_cannot_report(client, db_session, setting): + owner = await _register_and_login(client, "owner_nodetok") + gid = await _public_group(client, db_session, owner, "nodetok-grp") + node = await _node_headers(client, "node_reporter") + assert (await _report(client, node, gid)).status_code == 403 @pytest.mark.asyncio -async def test_same_reporter_cannot_walk_the_threshold(client, reporter): +async def test_a_new_account_cannot_report_yet(client, db_session, admin_headers): + owner = await _register_and_login(client, "owner_young") + gid = await _public_group(client, db_session, owner, "young-grp") + [young] = await _members(client, gid, ["young_member"]) + r = await _report(client, young, gid) + assert r.status_code == 403 and "too new" in r.json()["detail"] + + +@pytest.mark.asyncio +async def test_only_a_member_of_that_public_group_may_report(client, db_session, setting): + owner = await _register_and_login(client, "owner_member") + gid = await _public_group(client, db_session, owner, "member-grp") + stranger = await _register_and_login(client, "stranger_one") + private = (await client.post("/v1/groups", json={"name": "priv-mod"}, + headers=owner)).json()["group_id"] + answers = {(await _report(client, stranger, gid)).json()["detail"], + (await _report(client, owner, private)).json()["detail"], + (await _report(client, stranger, "no-such-group")).json()["detail"]} + # One uniform refusal: it must not say which groups exist or who is in them. + assert len(answers) == 1 + assert (await _report(client, owner, gid)).status_code == 201 + + +@pytest.mark.asyncio +async def test_a_report_says_nothing_about_how_close_review_is(client, db_session, setting): + owner = await _register_and_login(client, "owner_quiet") + gid = await _public_group(client, db_session, owner, "quiet-grp") + r = await _report(client, owner, gid) + assert r.status_code == 201 and r.json() == {"status": "logged"} + + +@pytest.mark.asyncio +async def test_same_reporter_cannot_walk_the_threshold(client, db_session, setting): + await _policy(client, setting, review_threshold=1) + owner = await _register_and_login(client, "owner_walk") + gid = await _public_group(client, db_session, owner, "walk-grp") h = "b" * 64 - for _ in range(5): - r = await client.post("/v1/reports", - json={"content_hash": h, "reason": "spam"}, - headers=reporter) - assert r.json()["report_count"] == 1 + [m] = await _members(client, gid, ["walker_one"]) + await _report(client, m, gid, h) + for _ in range(4): + r = await _report(client, m, gid, h) assert r.json()["status"] == "already_reported" - assert not await _blocked(client, h) - @pytest.mark.asyncio -async def test_auto_block_on_distinct_reporters(client): +async def test_reaching_the_threshold_queues_for_an_administrator(client, db_session, setting): + owner = await _register_and_login(client, "owner_queue") + gid = await _public_group(client, db_session, owner, "queue-grp") h = "c" * 64 - for i in range(3): - headers = await _register_and_login(client, f"reporter_{i}") - r = await client.post("/v1/reports", - json={"content_hash": h, "reason": "illegal"}, - headers=headers) - assert r.json()["status"] == "auto_blocked" - assert r.json()["report_count"] == 3 + for m in await _members(client, gid, ["queue_r0", "queue_r1", "queue_r2"]): + assert (await _report(client, m, gid, h)).status_code == 201 + + assert not await _blocked(client, h), "nothing is blocked without a decision" + queue = (await client.get("/v1/admin/reports", headers=setting)).json()["reports"] + [item] = [q for q in queue if q["hash"] == h] + assert item["reporters"] == 3 and item["reasons"] == {"illegal": 3} + assert item["groups"] == [{"id": gid, "name": "queue-grp"}] + notes = (await client.get("/v1/notifications", headers=setting)).json() + assert any(n["kind"] == "content_review" for n in notes["notifications"]) + r = await client.post(f"/v1/admin/reports/{h}/block", headers=setting) + assert r.status_code == 200 assert await _blocked(client, h) + queue = (await client.get("/v1/admin/reports", headers=setting)).json()["reports"] + assert h not in [q["hash"] for q in queue] @pytest.mark.asyncio -async def test_reports_refused_when_public_groups_disabled(client, reporter, admin_headers): - await client.patch("/v1/admin/settings", - json={"allow_public_groups": False}, - headers=admin_headers) +async def test_a_dismissed_report_stays_dismissed(client, db_session, setting): + await _policy(client, setting, review_threshold=1) + owner = await _register_and_login(client, "owner_dismiss") + gid = await _public_group(client, db_session, owner, "dismiss-grp") + h = "d" * 64 + [a, b] = await _members(client, gid, ["dismiss_a", "dismiss_b"]) + await _report(client, a, gid, h) + assert (await client.post(f"/v1/admin/reports/{h}/dismiss", + headers=setting)).status_code == 200 + await _report(client, b, gid, h) + queue = (await client.get("/v1/admin/reports", headers=setting)).json()["reports"] + assert h not in [q["hash"] for q in queue] + assert not await _blocked(client, h) - r = await client.post("/v1/reports", - json={"content_hash": "d" * 64, "reason": "illegal"}, - headers=reporter) + +@pytest.mark.asyncio +async def test_automatic_blocking_is_the_instances_choice(client, db_session, setting): + await _policy(client, setting, review_threshold=2, auto_block=1) + owner = await _register_and_login(client, "owner_auto") + gid = await _public_group(client, db_session, owner, "auto-grp") + h = "e" * 64 + for m in await _members(client, gid, ["autoblock_r0", "autoblock_r1"]): + await _report(client, m, gid, h) + assert await _blocked(client, h) + + +@pytest.mark.asyncio +async def test_a_member_has_a_daily_allowance(client, db_session, setting): + await _policy(client, setting, daily_per_account=2) + owner = await _register_and_login(client, "owner_daily") + gid = await _public_group(client, db_session, owner, "daily-grp") + for i in range(2): + assert (await _report(client, owner, gid, f"{i:064x}")).status_code == 201 + assert (await _report(client, owner, gid, f"{9:064x}")).status_code == 429 + + +@pytest.mark.asyncio +async def test_a_report_is_shaped(client, db_session, setting): + owner = await _register_and_login(client, "owner_shape") + gid = await _public_group(client, db_session, owner, "shape-grp") + assert (await _report(client, owner, gid, "not-a-hash")).status_code == 422 + assert (await _report(client, owner, gid, reason="because")).status_code == 422 + assert (await _report(client, owner, gid, detail="x" * 257)).status_code == 422 + + +@pytest.mark.asyncio +async def test_only_an_administrator_decides(client, db_session, setting): + await _policy(client, setting, review_threshold=1) + owner = await _register_and_login(client, "owner_decide") + gid = await _public_group(client, db_session, owner, "decide-grp") + await _report(client, owner, gid, "f" * 64) + r = await client.post(f"/v1/admin/reports/{'f' * 64}/block", headers=owner) assert r.status_code == 403 @pytest.mark.asyncio -async def test_invalid_hash_rejected(client, reporter): - r = await client.post("/v1/reports", - json={"content_hash": "not-a-valid-blake3-hash", - "reason": "test"}, - headers=reporter) - assert r.status_code == 422 +async def test_reports_refused_when_public_groups_disabled(client, db_session, setting): + owner = await _register_and_login(client, "owner_off") + gid = await _public_group(client, db_session, owner, "off-grp") + await client.patch("/v1/admin/settings", json={"allow_public_groups": False}, + headers=setting) + assert (await _report(client, owner, gid)).status_code == 403 @pytest.mark.asyncio |