aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_moderation.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-09 05:17:28 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-09 05:17:28 +0200
commit42556800d103ede20b4e97f2d91d20bbc0000c1e (patch)
tree1a392221995f4e8092bd20fb3acdd95a28c1d1f1 /packages/meshbay-hub/tests/test_moderation.py
parent1734c668406c66e2be63e0c6999b4b2af2f60808 (diff)
downloadmeshbay-42556800d103ede20b4e97f2d91d20bbc0000c1e.tar.gz
feat(hub): add moderation — content blocklist + reports — 5.9
DB: ContentReport + ContentBlocklist tables. POST /v1/reports: public endpoint, auto-blocks after 2 reports. GET /v1/blocklist/check: node sync check before serving public content. GET /v1/blocklist: full list for node startup sync. GET|POST|DELETE /v1/admin/blocklist: admin management. 6/6 tests. Full suite: 59/59. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_moderation.py')
-rw-r--r--packages/meshbay-hub/tests/test_moderation.py95
1 files changed, 95 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_moderation.py b/packages/meshbay-hub/tests/test_moderation.py
new file mode 100644
index 0000000..68e08c9
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_moderation.py
@@ -0,0 +1,95 @@
+"""Tests for moderation — reports + blocklist."""
+
+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
+
+
+FAKE_HASH = "a" * 64 # valid blake3 hex
+
+
+@pytest.fixture
+async def auth_headers(client):
+ sk_ed = Ed25519PrivateKey.generate()
+ sk_x = X25519PrivateKey.generate()
+ 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()),
+ })
+ r = await client.post("/v1/users/login",
+ json={"username": "mod_admin", "password": "modpass99"})
+ return {"Authorization": f"Bearer {r.json()['access_token']}"}
+
+
+@pytest.mark.asyncio
+async def test_report_content_logged(client):
+ r = await client.post("/v1/reports", json={
+ "content_hash": FAKE_HASH, "reason": "illegal"})
+ assert r.status_code == 201
+ data = r.json()
+ assert data["report_count"] == 1
+ assert data["status"] == "logged"
+
+
+@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"})
+ assert r.json()["status"] == "auto_blocked"
+ assert r.json()["report_count"] == 2
+
+
+@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
+
+ # 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.get(f"/v1/blocklist/check?hash={hash3}")
+ assert r.json()["blocked"] is True
+
+
+@pytest.mark.asyncio
+async def test_admin_add_remove_blocklist(client, auth_headers):
+ hash4 = "d" * 64
+
+ r = await client.post("/v1/admin/blocklist",
+ json={"content_hash": hash4, "reason": "csam"},
+ headers=auth_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)
+ 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_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
+ await client.post("/v1/admin/blocklist",
+ json={"content_hash": hash5, "reason": "test"},
+ headers=auth_headers)
+ r = await client.get("/v1/blocklist")
+ assert r.status_code == 200
+ assert hash5 in r.json()["hashes"]