Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
4 "encoding/json"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "testing"
9
10 "tangled.org/core/api/tangled"
11)
12
13func listRequest(t *testing.T, params url.Values) *http.Request {
14 t.Helper()
15 return httptest.NewRequest(http.MethodGet, "/xrpc/test?"+params.Encode(), nil)
16}
17
18func decodeMembers(t *testing.T, rec *httptest.ResponseRecorder) tangled.KnotListMembers_Output {
19 t.Helper()
20 var out tangled.KnotListMembers_Output
21 if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
22 t.Fatalf("decode members: %v", err)
23 }
24 return out
25}
26
27func seedMembers(t *testing.T, x *Xrpc, subjects ...string) {
28 t.Helper()
29 for _, s := range subjects {
30 rec := httptest.NewRecorder()
31 x.AddMember(rec, aclRequest(t, aclOwner, tangled.KnotAddMember_Input{Subject: s}))
32 if rec.Code != http.StatusOK {
33 t.Fatalf("seed member %s: status %d, body=%s", s, rec.Code, rec.Body.String())
34 }
35 }
36}
37
38func TestListMembers_ReturnsAddedSubjects(t *testing.T) {
39 x, _ := newACLXrpc(t)
40 seedMembers(t, x, aclSubject, "did:plc:scallop")
41
42 rec := httptest.NewRecorder()
43 x.ListMembers(rec, listRequest(t, url.Values{"subject": {"knot.example"}}))
44 if rec.Code != http.StatusOK {
45 t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
46 }
47
48 out := decodeMembers(t, rec)
49 if len(out.Items) != 2 {
50 t.Fatalf("items = %d, want 2", len(out.Items))
51 }
52 if out.Cursor != nil {
53 t.Errorf("cursor = %q, want nil for a complete page", *out.Cursor)
54 }
55
56 for _, it := range out.Items {
57 if it.AddedBy != aclOwner {
58 t.Errorf("subject %s addedBy = %q, want %s", it.Subject, it.AddedBy, aclOwner)
59 }
60 if it.CreatedAt == "" {
61 t.Errorf("subject %s has empty createdAt", it.Subject)
62 }
63 if it.Uri != nil || it.Cid != nil {
64 t.Errorf("subject %s carries record uri/cid; a knot must omit them", it.Subject)
65 }
66 }
67}
68
69func TestListMembers_Empty(t *testing.T) {
70 x, _ := newACLXrpc(t)
71 rec := httptest.NewRecorder()
72 x.ListMembers(rec, listRequest(t, url.Values{"subject": {"knot.example"}}))
73 if rec.Code != http.StatusOK {
74 t.Fatalf("status = %d, want 200", rec.Code)
75 }
76 out := decodeMembers(t, rec)
77 if len(out.Items) != 0 {
78 t.Errorf("items = %d, want 0", len(out.Items))
79 }
80}
81
82func TestListMembers_RejectsMalformedParams(t *testing.T) {
83 x, _ := newACLXrpc(t)
84 for _, q := range []url.Values{
85 {"limit": {"abc"}},
86 {"cursor": {"notanint"}},
87 {"order": {"ascending"}},
88 } {
89 rec := httptest.NewRecorder()
90 x.ListMembers(rec, listRequest(t, q))
91 if rec.Code != http.StatusBadRequest {
92 t.Errorf("params %v: status = %d, want 400", q, rec.Code)
93 }
94 }
95}
96
97func TestListMembers_ClampsOutOfRangeLimit(t *testing.T) {
98 x, _ := newACLXrpc(t)
99 seedMembers(t, x, aclSubject)
100 rec := httptest.NewRecorder()
101 x.ListMembers(rec, listRequest(t, url.Values{"limit": {"5000"}}))
102 if rec.Code != http.StatusOK {
103 t.Fatalf("limit=5000 should clamp and return 200, got %d; body=%s", rec.Code, rec.Body.String())
104 }
105 if len(decodeMembers(t, rec).Items) != 1 {
106 t.Error("clamped limit must still return the seeded member")
107 }
108}
109
110func TestListMembers_PaginatesWithoutOverlap(t *testing.T) {
111 x, _ := newACLXrpc(t)
112 all := []string{"did:plc:scallop", "did:plc:whelk", "did:plc:limpet"}
113 seedMembers(t, x, all...)
114
115 seen := map[string]bool{}
116 params := url.Values{"subject": {"knot.example"}, "limit": {"2"}}
117
118 rec := httptest.NewRecorder()
119 x.ListMembers(rec, listRequest(t, params))
120 page1 := decodeMembers(t, rec)
121 if len(page1.Items) != 2 || page1.Cursor == nil {
122 t.Fatalf("page1 items=%d cursor=%v, want 2 items and a cursor", len(page1.Items), page1.Cursor)
123 }
124 for _, it := range page1.Items {
125 seen[it.Subject] = true
126 }
127
128 params.Set("cursor", *page1.Cursor)
129 rec = httptest.NewRecorder()
130 x.ListMembers(rec, listRequest(t, params))
131 page2 := decodeMembers(t, rec)
132 if len(page2.Items) != 1 {
133 t.Fatalf("page2 items = %d, want 1", len(page2.Items))
134 }
135 if page2.Cursor != nil {
136 t.Errorf("page2 cursor = %q, want nil at end", *page2.Cursor)
137 }
138 for _, it := range page2.Items {
139 if seen[it.Subject] {
140 t.Errorf("subject %s repeated across pages", it.Subject)
141 }
142 seen[it.Subject] = true
143 }
144
145 if len(seen) != len(all) {
146 t.Errorf("distinct subjects seen = %d, want %d", len(seen), len(all))
147 }
148}
149
150func TestListCollaborators_ScopedToRepo(t *testing.T) {
151 x, _ := newACLXrpc(t)
152 seedRepo(t, x)
153
154 addRec := httptest.NewRecorder()
155 x.AddCollaborator(addRec, aclRequest(t, aclOwner, tangled.RepoAddCollaborator_Input{Repo: aclRepoDid, Subject: aclSubject}))
156 if addRec.Code != http.StatusOK {
157 t.Fatalf("seed collaborator: status %d, body=%s", addRec.Code, addRec.Body.String())
158 }
159
160 rec := httptest.NewRecorder()
161 x.ListCollaborators(rec, listRequest(t, url.Values{"subject": {aclRepoDid}}))
162 if rec.Code != http.StatusOK {
163 t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
164 }
165
166 var out tangled.RepoListCollaborators_Output
167 if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
168 t.Fatalf("decode: %v", err)
169 }
170 if len(out.Items) != 1 {
171 t.Fatalf("items = %d, want 1", len(out.Items))
172 }
173 if out.Items[0].Subject != aclSubject {
174 t.Errorf("subject = %q, want %s", out.Items[0].Subject, aclSubject)
175 }
176 if out.Items[0].AddedBy != aclOwner {
177 t.Errorf("addedBy = %q, want %s", out.Items[0].AddedBy, aclOwner)
178 }
179 if out.Items[0].Uri != nil || out.Items[0].Cid != nil {
180 t.Error("collaborator carries record uri/cid; a knot must omit them")
181 }
182}
183
184func TestListCollaborators_MalformedSubjectBadRequest(t *testing.T) {
185 x, _ := newACLXrpc(t)
186 rec := httptest.NewRecorder()
187 x.ListCollaborators(rec, listRequest(t, url.Values{"subject": {"notadid"}}))
188 if rec.Code != http.StatusBadRequest {
189 t.Errorf("status = %d, want 400", rec.Code)
190 }
191}
192
193func TestListCollaborators_UnknownRepoEmpty(t *testing.T) {
194 x, _ := newACLXrpc(t)
195 rec := httptest.NewRecorder()
196 x.ListCollaborators(rec, listRequest(t, url.Values{"subject": {"did:plc:scallop"}}))
197 if rec.Code != http.StatusOK {
198 t.Fatalf("status = %d, want 200", rec.Code)
199 }
200 var out tangled.RepoListCollaborators_Output
201 if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
202 t.Fatalf("decode: %v", err)
203 }
204 if len(out.Items) != 0 {
205 t.Errorf("items = %d, want 0 for an unknown repo", len(out.Items))
206 }
207}