diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_moderation.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_moderation.py | 132 |
1 files changed, 87 insertions, 45 deletions
diff --git a/packages/meshbay-hub/tests/test_moderation.py b/packages/meshbay-hub/tests/test_moderation.py index 93d23cd..6848929 100644 --- a/packages/meshbay-hub/tests/test_moderation.py +++ b/packages/meshbay-hub/tests/test_moderation.py @@ -1,34 +1,58 @@ -"""Tests for moderation — reports + blocklist.""" +"""Tests for moderation — reports + blocklist. + +Reporting requires a signed-in account (it used to be anonymous, which made it a +network-wide censorship primitive), the auto-block threshold counts *distinct +reporting accounts*, and the whole flow is refused when the hub has public groups +switched off. +""" import pytest -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey -from meshbay_common.crypto import pk_to_b64 from meshbay_hub.api.deps import set_admin_usernames - FAKE_HASH = "a" * 64 # valid blake3 hex -@pytest.fixture -async def auth_headers(client): - sk_ed = Ed25519PrivateKey.generate() - sk_x = X25519PrivateKey.generate() +async def _register_and_login(client, username: str) -> dict: await client.post("/v1/users/register", json={ - "username": "mod_admin", "email": "m@t.com", "password": "modpass99", - "pk_user_ed25519": pk_to_b64(sk_ed.public_key()), - "pk_user_x25519": pk_to_b64(sk_x.public_key()), + "username": username, "email": f"{username}@t.com", + "password": "reporter99pw", }) r = await client.post("/v1/users/login", - json={"username": "mod_admin", "password": "modpass99"}) - set_admin_usernames(["mod_admin"]) + json={"username": username, "password": "reporter99pw"}) return {"Authorization": f"Bearer {r.json()['access_token']}"} +@pytest.fixture +async def reporter(client): + return await _register_and_login(client, "reporter_one") + + +@pytest.fixture +async def admin_headers(client): + headers = await _register_and_login(client, "mod_admin") + set_admin_usernames(["mod_admin"]) + return headers + + @pytest.mark.asyncio -async def test_report_content_logged(client): +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"}) + 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"}, + 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 @@ -36,43 +60,68 @@ async def test_report_content_logged(client): @pytest.mark.asyncio -async def test_auto_block_on_threshold(client): - """Second report triggers auto-block.""" - hash2 = "b" * 64 - await client.post("/v1/reports", json={"content_hash": hash2, "reason": "spam"}) - r = await client.post("/v1/reports", json={"content_hash": hash2, "reason": "spam"}) +async def test_same_reporter_cannot_walk_the_threshold(client, reporter): + 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 + assert r.json()["status"] == "already_reported" + + check = await client.get(f"/v1/blocklist/check?hash={h}") + assert check.json()["blocked"] is False + + +@pytest.mark.asyncio +async def test_auto_block_on_distinct_reporters(client): + h = "c" * 64 + for i in range(3): + headers = await _register_and_login(client, f"rep_{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"] == 2 + assert r.json()["report_count"] == 3 + + check = await client.get(f"/v1/blocklist/check?hash={h}") + assert check.json()["blocked"] is True @pytest.mark.asyncio -async def test_blocklist_check(client): - hash3 = "c" * 64 - # Not blocked yet - r = await client.get(f"/v1/blocklist/check?hash={hash3}") - assert r.json()["blocked"] is False +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) - # Report twice to auto-block - await client.post("/v1/reports", json={"content_hash": hash3, "reason": "illegal"}) - await client.post("/v1/reports", json={"content_hash": hash3, "reason": "illegal"}) + r = await client.post("/v1/reports", + json={"content_hash": "d" * 64, "reason": "illegal"}, + headers=reporter) + assert r.status_code == 403 - r = await client.get(f"/v1/blocklist/check?hash={hash3}") - assert r.json()["blocked"] is True + +@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 @pytest.mark.asyncio -async def test_admin_add_remove_blocklist(client, auth_headers): - hash4 = "d" * 64 +async def test_admin_add_remove_blocklist(client, admin_headers): + hash4 = "e" * 64 r = await client.post("/v1/admin/blocklist", json={"content_hash": hash4, "reason": "csam"}, - headers=auth_headers) + headers=admin_headers) assert r.status_code == 201 r = await client.get(f"/v1/blocklist/check?hash={hash4}") assert r.json()["blocked"] is True - r = await client.delete(f"/v1/admin/blocklist/{hash4}", headers=auth_headers) + r = await client.delete(f"/v1/admin/blocklist/{hash4}", headers=admin_headers) assert r.status_code == 200 r = await client.get(f"/v1/blocklist/check?hash={hash4}") @@ -80,18 +129,11 @@ async def test_admin_add_remove_blocklist(client, auth_headers): @pytest.mark.asyncio -async def test_invalid_hash_rejected(client): - r = await client.post("/v1/reports", json={ - "content_hash": "not-a-valid-blake3-hash", "reason": "test"}) - assert r.status_code == 422 - - -@pytest.mark.asyncio -async def test_full_blocklist(client, auth_headers): - hash5 = "e" * 64 +async def test_full_blocklist(client, admin_headers): + hash5 = "f" * 64 await client.post("/v1/admin/blocklist", json={"content_hash": hash5, "reason": "test"}, - headers=auth_headers) + headers=admin_headers) r = await client.get("/v1/blocklist") assert r.status_code == 200 assert hash5 in r.json()["hashes"] |