"""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 meshbay_hub.api.deps import set_admin_usernames FAKE_HASH = "a" * 64 # valid blake3 hex async def _register_and_login(client, username: str) -> dict: await client.post("/v1/users/register", json={ "username": username, "email": f"{username}@t.com", "password": "reporter99pw", }) r = await client.post("/v1/users/login", 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_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 assert data["status"] == "logged" @pytest.mark.asyncio 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"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 check = await client.get(f"/v1/blocklist/check?hash={h}") assert check.json()["blocked"] is True @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) r = await client.post("/v1/reports", json={"content_hash": "d" * 64, "reason": "illegal"}, headers=reporter) 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 @pytest.mark.asyncio 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=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=admin_headers) assert r.status_code == 200 r = await client.get(f"/v1/blocklist/check?hash={hash4}") assert r.json()["blocked"] is False @pytest.mark.asyncio async def test_full_blocklist(client, admin_headers): hash5 = "f" * 64 await client.post("/v1/admin/blocklist", json={"content_hash": hash5, "reason": "test"}, headers=admin_headers) r = await client.get("/v1/blocklist") assert r.status_code == 200 assert hash5 in r.json()["hashes"]