From cc90dc943fcd0e7bbf52674fb3f95ff097026f4a Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 11 Aug 2026 14:55:52 +0200 Subject: feat: Phase 10b — Self-service UI (group create/join, upload, IndexedDB, search) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six self-service features for the web SPA: - Group creation UI with GEK auto-generation (AES-256-GCM ECIES) - Member management + invite by username (GEK wrapping for invitee) - Open group self-join flow (POST /v1/groups/{id}/join) - File upload client→node (FILE_UPLOAD MNP type, .uploads/ staging) - IndexedDB caching of group file indexes (instant display on revisit) - Cross-group file search (SearchPage, pure client-side on cached indexes) 11 new tests (166 total): 8 group self-service + 3 AES GEK wrap/unwrap. Co-Authored-By: Claude Opus 4.6 --- .../meshbay-hub/tests/test_groups_self_service.py | 161 +++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 packages/meshbay-hub/tests/test_groups_self_service.py (limited to 'packages/meshbay-hub/tests/test_groups_self_service.py') diff --git a/packages/meshbay-hub/tests/test_groups_self_service.py b/packages/meshbay-hub/tests/test_groups_self_service.py new file mode 100644 index 0000000..a60209d --- /dev/null +++ b/packages/meshbay-hub/tests/test_groups_self_service.py @@ -0,0 +1,161 @@ +"""Integration tests for group self-service: create, join, members.""" + +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 + + +def _gen_user_keys(): + sk_ed = Ed25519PrivateKey.generate() + sk_x = X25519PrivateKey.generate() + return pk_to_b64(sk_ed.public_key()), pk_to_b64(sk_x.public_key()) + + +async def _register(client, username, email="test@x.com", password="testpass99"): + pk_ed, pk_x = _gen_user_keys() + r = await client.post("/v1/users/register", json={ + "username": username, "email": email, "password": password, + "pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x, + }) + assert r.status_code == 201 + return r.json()["user_id"] + + +async def _login(client, username, password="testpass99"): + r = await client.post("/v1/users/login", json={ + "username": username, "password": password, + }) + assert r.status_code == 200 + return r.json()["access_token"] + + +async def _create_group(client, token, name="test-group", visibility="public", + join_policy="open"): + r = await client.post("/v1/groups", json={ + "name": name, "visibility": visibility, "join_policy": join_policy, + }, headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 201 + return r.json()["group_id"] + + +@pytest.mark.asyncio +async def test_create_group(client): + await _register(client, "alice", email="a@x.com") + token = await _login(client, "alice") + + r = await client.post("/v1/groups", json={ + "name": "my-group", "visibility": "public", "join_policy": "open", + }, headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 201 + data = r.json() + assert data["name"] == "my-group" + assert "group_id" in data + + +@pytest.mark.asyncio +async def test_join_open_group(client): + await _register(client, "alice", email="a@x.com") + alice_token = await _login(client, "alice") + gid = await _create_group(client, alice_token, "open-group") + + await _register(client, "bob", email="b@x.com") + bob_token = await _login(client, "bob") + + r = await client.post(f"/v1/groups/{gid}/join", + headers={"Authorization": f"Bearer {bob_token}"}) + assert r.status_code == 200 + assert r.json()["status"] == "joined" + + +@pytest.mark.asyncio +async def test_join_invite_group_rejected(client): + await _register(client, "alice", email="a@x.com") + alice_token = await _login(client, "alice") + gid = await _create_group(client, alice_token, "invite-group", + join_policy="invite") + + await _register(client, "bob", email="b@x.com") + bob_token = await _login(client, "bob") + + r = await client.post(f"/v1/groups/{gid}/join", + headers={"Authorization": f"Bearer {bob_token}"}) + assert r.status_code == 403 + + +@pytest.mark.asyncio +async def test_join_already_member(client): + await _register(client, "alice", email="a@x.com") + alice_token = await _login(client, "alice") + gid = await _create_group(client, alice_token, "dup-group") + + r = await client.post(f"/v1/groups/{gid}/join", + headers={"Authorization": f"Bearer {alice_token}"}) + assert r.status_code == 409 + + +@pytest.mark.asyncio +async def test_group_members(client): + await _register(client, "alice", email="a@x.com") + alice_token = await _login(client, "alice") + gid = await _create_group(client, alice_token, "team-group") + + await _register(client, "bob", email="b@x.com") + bob_token = await _login(client, "bob") + await client.post(f"/v1/groups/{gid}/join", + headers={"Authorization": f"Bearer {bob_token}"}) + + r = await client.get(f"/v1/groups/{gid}/members", + headers={"Authorization": f"Bearer {alice_token}"}) + assert r.status_code == 200 + data = r.json() + usernames = [m["username"] for m in data["members"]] + assert "alice" in usernames + assert "bob" in usernames + assert data["admin_id"] is not None + + +@pytest.mark.asyncio +async def test_group_members_non_member_denied(client): + await _register(client, "alice", email="a@x.com") + alice_token = await _login(client, "alice") + gid = await _create_group(client, alice_token, "private-group", + visibility="private", join_policy="invite") + + await _register(client, "bob", email="b@x.com") + bob_token = await _login(client, "bob") + + r = await client.get(f"/v1/groups/{gid}/members", + headers={"Authorization": f"Bearer {bob_token}"}) + assert r.status_code == 403 + + +@pytest.mark.asyncio +async def test_group_search(client): + await _register(client, "alice", email="a@x.com") + token = await _login(client, "alice") + await _create_group(client, token, "alpha-team") + await _create_group(client, token, "beta-team") + + r = await client.get("/v1/groups?q=alpha") + assert r.status_code == 200 + names = [g["name"] for g in r.json()["groups"]] + assert "alpha-team" in names + assert "beta-team" not in names + + +@pytest.mark.asyncio +async def test_join_triggers_notification(client): + await _register(client, "alice", email="a@x.com") + alice_token = await _login(client, "alice") + gid = await _create_group(client, alice_token, "notif-group") + + await _register(client, "bob", email="b@x.com") + bob_token = await _login(client, "bob") + await client.post(f"/v1/groups/{gid}/join", + headers={"Authorization": f"Bearer {bob_token}"}) + + r = await client.get(f"/v1/groups/{gid}/members", + headers={"Authorization": f"Bearer {alice_token}"}) + assert len(r.json()["members"]) == 2 -- cgit v1.2.3