diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-11 04:13:53 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-11 04:13:53 +0200 |
| commit | e23e33adeaf8ee7439187d4451c856b37816a51f (patch) | |
| tree | a41eef1fba34cdd642d25576395b4ad484a748ad /packages/meshbay-hub/tests | |
| parent | 60c4570e72e36c2a9720593c8baec74ee2ab52d6 (diff) | |
| download | meshbay-e23e33adeaf8ee7439187d4451c856b37816a51f.tar.gz | |
feat: Phase 9 — Web client SPA with WebRTC P2P transport
Complete browser-based client: Preact SPA with login, group file browser,
encrypted download, video playback, group chat, i18n, and dark/light theme.
Browser connects P2P to nodes behind residential NAT via WebRTC DataChannel
(aiortc). Hub handles signaling only — all data flows E2E.
Performance: pipelined downloads (8-chunk sliding window), binary msgpack
wire format (no base64), redundant I/O elimination. Large file downloads
stream to disk via File System Access API (showSaveFilePicker).
Validated on SFR + Orange residential NATs, Chrome + Firefox, IPv4/IPv6.
132 tests passing. Deployed to meshbay.org + Orange node.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_hub_api.py | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_hub_api.py b/packages/meshbay-hub/tests/test_hub_api.py index a9c97a3..f7c499e 100644 --- a/packages/meshbay-hub/tests/test_hub_api.py +++ b/packages/meshbay-hub/tests/test_hub_api.py @@ -351,6 +351,131 @@ async def test_jwt_contains_groups_claim(client): assert group_id in decoded_alice["groups"] +# ── My groups (9.6) ───────────────────────────────────────────────────────── + +@pytest.mark.asyncio +async def test_my_groups(client): + """GET /v1/groups/mine returns groups the user belongs to.""" + pk_ed_a, pk_x_a, _ = _gen_user_keys() + pk_ed_b, pk_x_b, _ = _gen_user_keys() + + await client.post("/v1/users/register", json={ + "username": "mg_alice", "email": "mga@x.com", "password": "alicepass99", + "pk_user_ed25519": pk_ed_a, "pk_user_x25519": pk_x_a}) + await client.post("/v1/users/register", json={ + "username": "mg_bob", "email": "mgb@x.com", "password": "bobpass99", + "pk_user_ed25519": pk_ed_b, "pk_user_x25519": pk_x_b}) + + alice_token = (await client.post("/v1/users/login", + json={"username": "mg_alice", "password": "alicepass99"})).json()["access_token"] + bob_token = (await client.post("/v1/users/login", + json={"username": "mg_bob", "password": "bobpass99"})).json()["access_token"] + + # Bob has no groups initially + r = await client.get("/v1/groups/mine", + headers={"Authorization": f"Bearer {bob_token}"}) + assert r.status_code == 200 + assert r.json()["groups"] == [] + + # Alice creates a group and adds Bob + r = await client.post("/v1/groups", json={"name": "mg-group"}, + headers={"Authorization": f"Bearer {alice_token}"}) + group_id = r.json()["group_id"] + gek = generate_gek() + bundle = wrap_gek(gek, base64.b64decode(pk_x_b)) + await client.post(f"/v1/groups/{group_id}/members/mg_bob/gek", + json=bundle, + headers={"Authorization": f"Bearer {alice_token}"}) + + # Re-login to get fresh token with group claims + bob_token = (await client.post("/v1/users/login", + json={"username": "mg_bob", "password": "bobpass99"})).json()["access_token"] + + # Now Bob should see the group + r = await client.get("/v1/groups/mine", + headers={"Authorization": f"Bearer {bob_token}"}) + assert r.status_code == 200 + groups = r.json()["groups"] + assert len(groups) == 1 + assert groups[0]["id"] == group_id + assert groups[0]["name"] == "mg-group" + assert groups[0]["is_admin"] is False + + # Alice should see it too, with is_admin=True + alice_token = (await client.post("/v1/users/login", + json={"username": "mg_alice", "password": "alicepass99"})).json()["access_token"] + r = await client.get("/v1/groups/mine", + headers={"Authorization": f"Bearer {alice_token}"}) + groups = r.json()["groups"] + assert any(g["id"] == group_id and g["is_admin"] for g in groups) + + # Unauthenticated → rejected + r = await client.get("/v1/groups/mine") + assert r.status_code >= 400 + + +@pytest.mark.asyncio +async def test_group_online_nodes(client): + """GET /v1/groups/{id}/nodes returns online nodes serving the group.""" + import json + from meshbay_hub.api.revocation import _connected_nodes, _node_groups + + pk_ed, pk_x, _ = _gen_user_keys() + await client.post("/v1/users/register", json={ + "username": "gn_user", "email": "gn@x.com", "password": "gnpass999", + "pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x}) + r = await client.post("/v1/users/login", + json={"username": "gn_user", "password": "gnpass999"}) + token = r.json()["access_token"] + + r = await client.post("/v1/groups", json={"name": "gn-group"}, + headers={"Authorization": f"Bearer {token}"}) + group_id = r.json()["group_id"] + + # Re-login to get fresh token with group claims + token = (await client.post("/v1/users/login", + json={"username": "gn_user", "password": "gnpass999"})).json()["access_token"] + + # Announce a node + r = await client.post("/v1/nodes/announce", json={ + "pk_node": pk_ed, "endpoint_hint": "1.2.3.4:19000"}, + headers={"Authorization": f"Bearer {token}"}) + node_id = r.json()["node_id"] + + # No nodes online yet + r = await client.get(f"/v1/groups/{group_id}/nodes", + headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 200 + assert r.json()["nodes"] == [] + + # Simulate node connecting via WS with group_ids + class FakeWS: + async def send_text(self, text): pass + _connected_nodes[node_id] = FakeWS() + _node_groups[node_id] = [group_id] + + try: + r = await client.get(f"/v1/groups/{group_id}/nodes", + headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 200 + nodes = r.json()["nodes"] + assert len(nodes) == 1 + assert nodes[0]["node_id"] == node_id + assert nodes[0]["pk_node"] == pk_ed + finally: + _connected_nodes.pop(node_id, None) + _node_groups.pop(node_id, None) + + # 404 for nonexistent group + r = await client.get("/v1/groups/fake-id/nodes", + headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 404 + + # Unauthenticated → rejected + r = await client.get(f"/v1/groups/{group_id}/nodes") + assert r.status_code >= 400 + + # ── Admin authz (8.1) ─────────────────────────────────────────────────────── @pytest.mark.asyncio @@ -578,3 +703,22 @@ async def test_ip_log_cleanup(app): )).scalar_one() assert count == 1 break + + +# ── Webapp HTML shell (9.13) ───────────────────────────────────────────────── + +@pytest.mark.asyncio +async def test_webapp_html_includes_scripts(client): + """SPA HTML shell includes all required script tags.""" + r = await client.get("/") + assert r.status_code == 200 + html = r.text + assert "<!DOCTYPE html>" in html + assert '<div id="app">' in html + assert 'src="/keyderive.js"' in html + assert 'src="/crypto.js"' in html + assert 'src="/transport.js"' in html + assert 'src="/app.js"' in html + assert 'type="module"' in html + assert 'rel="stylesheet"' in html + assert 'href="/style.css"' in html |