eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "encoding/json"
5 "net/http"
6 "strings"
7 "testing"
8)
9
10// joinTestMember creates a user, claims a device token for them, and registers a
11// public key so they appear on the roster (joined members only).
12func joinTestMember(t *testing.T, srv *Server, inviteID string) (uid, tok string) {
13 t.Helper()
14 uid, _ = srv.db.createUser("")
15 srv.db.createKeyInvite(inviteID, uid, "", testClaimHash, 0)
16 tok, _, _ = srv.db.claimKeyInvite(inviteID, "", "", testClaimSecret)
17 srv.db.sql.Exec("UPDATE users SET public_key = 'pk' WHERE id = ?", uid)
18 return uid, tok
19}
20
21func TestPersonDescriptionBlob(t *testing.T) {
22 ts, srv := newTestServer(t)
23 uid, tok := joinTestMember(t, srv, "desc")
24
25 post := func(body string) *http.Response {
26 resp, err := http.Post(ts.URL+"/api/description", "application/json", strings.NewReader(body))
27 if err != nil {
28 t.Fatalf("post description: %v", err)
29 }
30 return resp
31 }
32
33 // Set: the roster carries the opaque blob.
34 if resp := post(`{"token":"` + tok + `","description_blob":"enc-about"}`); resp.StatusCode != 200 {
35 t.Fatalf("set: want 200, got %d", resp.StatusCode)
36 }
37 roster, _ := srv.db.roster()
38 var e *RosterEntry
39 for i := range roster {
40 if roster[i].UserID == uid {
41 e = &roster[i]
42 }
43 }
44 if e == nil || e.DescriptionBlob != "enc-about" {
45 t.Fatalf("roster entry after set: %+v", e)
46 }
47
48 // Clear: "" empties it again.
49 if resp := post(`{"token":"` + tok + `","description_blob":""}`); resp.StatusCode != 200 {
50 t.Fatalf("clear: want 200, got %d", resp.StatusCode)
51 }
52 roster, _ = srv.db.roster()
53 for i := range roster {
54 if roster[i].UserID == uid && roster[i].DescriptionBlob != "" {
55 t.Fatalf("clear left residue: %+v", roster[i])
56 }
57 }
58
59 // Oversized blobs and non-members are rejected.
60 if resp := post(`{"token":"` + tok + `","description_blob":"` + strings.Repeat("x", 5000) + `"}`); resp.StatusCode != 400 {
61 t.Fatalf("oversize: want 400, got %d", resp.StatusCode)
62 }
63 if resp := post(`{"token":"nope","description_blob":"enc"}`); resp.StatusCode != 403 {
64 t.Fatalf("bad token: want 403, got %d", resp.StatusCode)
65 }
66}
67
68func TestGroupDescription(t *testing.T) {
69 ts, srv := newTestServer(t)
70 _, memberTok := joinTestMember(t, srv, "gd-member")
71 adminUID, adminTok := joinTestMember(t, srv, "gd-admin")
72 srv.db.sql.Exec("UPDATE users SET role = 'admin' WHERE id = ?", adminUID)
73
74 post := func(bearer, body string) *http.Response {
75 req, _ := http.NewRequest("POST", ts.URL+"/api/group-description", strings.NewReader(body))
76 req.Header.Set("Authorization", "Bearer "+bearer)
77 req.Header.Set("Content-Type", "application/json")
78 resp, err := http.DefaultClient.Do(req)
79 if err != nil {
80 t.Fatalf("post group description: %v", err)
81 }
82 return resp
83 }
84 getJSON := func(url string) map[string]any {
85 resp, err := http.Get(url)
86 if err != nil {
87 t.Fatalf("get %s: %v", url, err)
88 }
89 var out map[string]any
90 json.NewDecoder(resp.Body).Decode(&out)
91 return out
92 }
93
94 // An ordinary member can't set it; a role-admin's own device token can.
95 if resp := post(memberTok, `{"description":"We talk about cheese."}`); resp.StatusCode != 401 {
96 t.Fatalf("member set: want 401, got %d", resp.StatusCode)
97 }
98 if resp := post(adminTok, `{"description":" We talk about cheese. "}`); resp.StatusCode != 200 {
99 t.Fatalf("role-admin set: want 200, got %d", resp.StatusCode)
100 }
101
102 // Members read it (trimmed) from /api/group; the unauthenticated onboarding
103 // read never includes it — invited people get it via the invite lookup below.
104 if g := getJSON(ts.URL + "/api/group?token=" + memberTok); g["description"] != "We talk about cheese." {
105 t.Fatalf("member read: %v", g["description"])
106 }
107 if g := getJSON(ts.URL + "/api/group"); g["description"] != nil {
108 t.Fatalf("public read leaked the description: %v", g["description"])
109 }
110
111 // A valid (unclaimed) invite serves it, so the join card can greet with it;
112 // a bogus invite id gets nothing.
113 pendingUID, _ := srv.db.createUser("")
114 srv.db.createKeyInvite("gd-pending", pendingUID, "", testClaimHash, 0)
115 if ki := getJSON(ts.URL + "/api/key-invite/gd-pending"); ki["ok"] != true || ki["description"] != "We talk about cheese." {
116 t.Fatalf("invite read: %v", ki)
117 }
118 if ki := getJSON(ts.URL + "/api/key-invite/no-such-invite"); ki["description"] != nil {
119 t.Fatalf("bogus invite got the description: %v", ki)
120 }
121
122 // The break-glass admin bearer token also passes, and "" clears it.
123 if resp := adminPost(t, ts.URL+"/api/group-description", `{"description":""}`); resp.StatusCode != 200 {
124 t.Fatalf("admin clear: want 200, got %d", resp.StatusCode)
125 }
126 if g := getJSON(ts.URL + "/api/group?token=" + memberTok); g["description"] != "" {
127 t.Fatalf("clear left residue: %v", g["description"])
128 }
129
130 // Oversized descriptions are rejected.
131 if resp := post(adminTok, `{"description":"`+strings.Repeat("x", 1500)+`"}`); resp.StatusCode != 400 {
132 t.Fatalf("oversize: want 400, got %d", resp.StatusCode)
133 }
134}
135