package main import ( "encoding/json" "net/http" "strings" "testing" ) // joinTestMember creates a user, claims a device token for them, and registers a // public key so they appear on the roster (joined members only). func joinTestMember(t *testing.T, srv *Server, inviteID string) (uid, tok string) { t.Helper() uid, _ = srv.db.createUser("") srv.db.createKeyInvite(inviteID, uid, "", testClaimHash, 0) tok, _, _ = srv.db.claimKeyInvite(inviteID, "", "", testClaimSecret) srv.db.sql.Exec("UPDATE users SET public_key = 'pk' WHERE id = ?", uid) return uid, tok } func TestPersonDescriptionBlob(t *testing.T) { ts, srv := newTestServer(t) uid, tok := joinTestMember(t, srv, "desc") post := func(body string) *http.Response { resp, err := http.Post(ts.URL+"/api/description", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("post description: %v", err) } return resp } // Set: the roster carries the opaque blob. if resp := post(`{"token":"` + tok + `","description_blob":"enc-about"}`); resp.StatusCode != 200 { t.Fatalf("set: want 200, got %d", resp.StatusCode) } roster, _ := srv.db.roster() var e *RosterEntry for i := range roster { if roster[i].UserID == uid { e = &roster[i] } } if e == nil || e.DescriptionBlob != "enc-about" { t.Fatalf("roster entry after set: %+v", e) } // Clear: "" empties it again. if resp := post(`{"token":"` + tok + `","description_blob":""}`); resp.StatusCode != 200 { t.Fatalf("clear: want 200, got %d", resp.StatusCode) } roster, _ = srv.db.roster() for i := range roster { if roster[i].UserID == uid && roster[i].DescriptionBlob != "" { t.Fatalf("clear left residue: %+v", roster[i]) } } // Oversized blobs and non-members are rejected. if resp := post(`{"token":"` + tok + `","description_blob":"` + strings.Repeat("x", 5000) + `"}`); resp.StatusCode != 400 { t.Fatalf("oversize: want 400, got %d", resp.StatusCode) } if resp := post(`{"token":"nope","description_blob":"enc"}`); resp.StatusCode != 403 { t.Fatalf("bad token: want 403, got %d", resp.StatusCode) } } func TestGroupDescription(t *testing.T) { ts, srv := newTestServer(t) _, memberTok := joinTestMember(t, srv, "gd-member") adminUID, adminTok := joinTestMember(t, srv, "gd-admin") srv.db.sql.Exec("UPDATE users SET role = 'admin' WHERE id = ?", adminUID) post := func(bearer, body string) *http.Response { req, _ := http.NewRequest("POST", ts.URL+"/api/group-description", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+bearer) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("post group description: %v", err) } return resp } getJSON := func(url string) map[string]any { resp, err := http.Get(url) if err != nil { t.Fatalf("get %s: %v", url, err) } var out map[string]any json.NewDecoder(resp.Body).Decode(&out) return out } // An ordinary member can't set it; a role-admin's own device token can. if resp := post(memberTok, `{"description":"We talk about cheese."}`); resp.StatusCode != 401 { t.Fatalf("member set: want 401, got %d", resp.StatusCode) } if resp := post(adminTok, `{"description":" We talk about cheese. "}`); resp.StatusCode != 200 { t.Fatalf("role-admin set: want 200, got %d", resp.StatusCode) } // Members read it (trimmed) from /api/group; the unauthenticated onboarding // read never includes it — invited people get it via the invite lookup below. if g := getJSON(ts.URL + "/api/group?token=" + memberTok); g["description"] != "We talk about cheese." { t.Fatalf("member read: %v", g["description"]) } if g := getJSON(ts.URL + "/api/group"); g["description"] != nil { t.Fatalf("public read leaked the description: %v", g["description"]) } // A valid (unclaimed) invite serves it, so the join card can greet with it; // a bogus invite id gets nothing. pendingUID, _ := srv.db.createUser("") srv.db.createKeyInvite("gd-pending", pendingUID, "", testClaimHash, 0) if ki := getJSON(ts.URL + "/api/key-invite/gd-pending"); ki["ok"] != true || ki["description"] != "We talk about cheese." { t.Fatalf("invite read: %v", ki) } if ki := getJSON(ts.URL + "/api/key-invite/no-such-invite"); ki["description"] != nil { t.Fatalf("bogus invite got the description: %v", ki) } // The break-glass admin bearer token also passes, and "" clears it. if resp := adminPost(t, ts.URL+"/api/group-description", `{"description":""}`); resp.StatusCode != 200 { t.Fatalf("admin clear: want 200, got %d", resp.StatusCode) } if g := getJSON(ts.URL + "/api/group?token=" + memberTok); g["description"] != "" { t.Fatalf("clear left residue: %v", g["description"]) } // Oversized descriptions are rejected. if resp := post(adminTok, `{"description":"`+strings.Repeat("x", 1500)+`"}`); resp.StatusCode != 400 { t.Fatalf("oversize: want 400, got %d", resp.StatusCode) } }