tinyrelayfile
tinyrelay/internal/daemon/collaboration_acceptance_test.go/

internal/daemon/collaboration_acceptance_test.go raw
1package daemon
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/url"
8 "strings"
9 "testing"
10
11 "github.com/FelineStateMachine/tinyrelay/internal/event"
12 "github.com/FelineStateMachine/tinyrelay/internal/relay"
13 "github.com/FelineStateMachine/tinyrelay/internal/storage"
14)
15
16const collaborationCoordinateRepo = "collaboration"
17
18func collaborationFixtureRepo(t *testing.T, tenant *Tenant) (string, string) {
19 t.Helper()
20 ctx := context.Background()
21 ownerSecret := strings.Repeat("0", 63) + "1"
22 owner := tenant.Policy().Owner
23 serviceURL, err := url.Parse(tenant.RelayURL())
24 if err != nil {
25 t.Fatal(err)
26 }
27 cloneURL := "http://" + serviceURL.Host + "/" + owner + "/" + collaborationCoordinateRepo + ".git"
28 relayURL := "ws://" + serviceURL.Host
29 announcement := event.Event{
30 Kind: event.KIND_REPO,
31 PubKey: owner,
32 CreatedAt: 100,
33 Tags: [][]string{{"d", collaborationCoordinateRepo}, {"clone", cloneURL}, {"relays", relayURL}},
34 }
35 if err := event.Sign(&announcement, ownerSecret); err != nil {
36 t.Fatal(err)
37 }
38 if _, err := tenant.router.Publish(ctx, announcement, relay.Session{PubKeys: []string{owner}}); err != nil {
39 t.Fatal(err)
40 }
41 return owner, "30617:" + owner + ":" + collaborationCoordinateRepo
42}
43
44func signedCollaborationEvent(t *testing.T, secret string, kind int, created int64, tags [][]string, content string) event.Event {
45 t.Helper()
46 e := event.Event{Kind: kind, CreatedAt: created, Tags: tags, Content: content}
47 if err := event.Sign(&e, secret); err != nil {
48 t.Fatal(err)
49 }
50 return e
51}
52
53func saveCollaborationEvents(t *testing.T, tenant *Tenant, rows ...event.Event) {
54 t.Helper()
55 for _, row := range rows {
56 if _, err := tenant.store.Save(context.Background(), row, storage.SaveOptions{Now: row.CreatedAt, SearchMode: tenant.Policy().Features.Search}); err != nil {
57 t.Fatal(err)
58 }
59 }
60}
61
62func browseCollaboration(t *testing.T, tenant *Tenant, actor, method string, args map[string]any) map[string]any {
63 t.Helper()
64 params, err := json.Marshal(args)
65 if err != nil {
66 t.Fatal(err)
67 }
68 value, err := tenant.Execute(context.Background(), actor, method, []json.RawMessage{params})
69 if err != nil {
70 t.Fatal(err)
71 }
72 result, ok := value.(map[string]any)
73 if !ok {
74 t.Fatalf("%s result = %T, want object", method, value)
75 }
76 return result
77}
78
79func browseCollaborationDetail(t *testing.T, tenant *Tenant, actor, method string, args map[string]any) collaborationDetail {
80 t.Helper()
81 params, err := json.Marshal(args)
82 if err != nil {
83 t.Fatal(err)
84 }
85 value, err := tenant.Execute(context.Background(), actor, method, []json.RawMessage{params})
86 if err != nil {
87 t.Fatal(err)
88 }
89 detail, ok := value.(collaborationDetail)
90 if !ok {
91 t.Fatalf("%s result = %T, want collaboration detail", method, value)
92 }
93 return detail
94}
95
96func TestCollaborationListPaginationKeepsTimestampTies(t *testing.T) {
97 _, tenant := testTenant(t)
98 p := tenant.Policy()
99 p.Features.Grasp = true
100 p.Features.Grasp02 = true
101 p.Features.Grasp05 = true
102 p.GuestReplies = true
103 if err := tenant.applyPolicy(context.Background(), p); err != nil {
104 t.Fatal(err)
105 }
106 owner, coordinate := collaborationFixtureRepo(t, tenant)
107 secret := strings.Repeat("1", 63) + "2"
108 var ids []string
109 for i := 0; i < 9; i++ {
110 row := signedCollaborationEvent(t, secret, event.KIND_GIT_ISSUE, 200,
111 [][]string{{"a", coordinate}, {"subject", fmt.Sprintf("Issue %02d", i)}}, "body")
112 saveCollaborationEvents(t, tenant, row)
113 ids = append(ids, row.ID)
114 }
115 seen := map[string]bool{}
116 cursor := ""
117 for page := 0; page < 20; page++ {
118 result := browseCollaboration(t, tenant, owner, "browseissues", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "limit": 2, "cursor": cursor})
119 items, ok := result["items"].([]collaborationItem)
120 if !ok {
121 t.Fatalf("items = %T", result["items"])
122 }
123 for _, item := range items {
124 id := item.ID
125 if seen[id] {
126 t.Fatalf("duplicate item %s", id)
127 }
128 seen[id] = true
129 }
130 cursor, _ = result["next_cursor"].(string)
131 if cursor == "" {
132 break
133 }
134 }
135 if len(seen) != len(ids) {
136 t.Fatalf("pagination returned %d issues, want %d", len(seen), len(ids))
137 }
138}
139
140func TestCollaborationSearchAndStatusReachPastFirstPage(t *testing.T) {
141 _, tenant := testTenant(t)
142 p := tenant.Policy()
143 p.Features.Grasp = true
144 p.Features.Grasp02 = true
145 p.Features.Grasp05 = true
146 p.GuestReplies = true
147 if err := tenant.applyPolicy(context.Background(), p); err != nil {
148 t.Fatal(err)
149 }
150 owner, coordinate := collaborationFixtureRepo(t, tenant)
151 secret := strings.Repeat("2", 63) + "3"
152 for i := 0; i < 60; i++ {
153 row := signedCollaborationEvent(t, secret, event.KIND_GIT_ISSUE, int64(300+i),
154 [][]string{{"a", coordinate}, {"subject", fmt.Sprintf("Routine issue %02d", i)}}, "ordinary body")
155 if i == 59 {
156 row.Tags = append(row.Tags, []string{"t", "security"})
157 row.Content = "Security regression"
158 }
159 saveCollaborationEvents(t, tenant, row)
160 if i == 59 {
161 status := signedCollaborationEvent(t, strings.Repeat("0", 63)+"1", 1632, 500,
162 [][]string{{"e", row.ID}}, "closed")
163 saveCollaborationEvents(t, tenant, status)
164 }
165 }
166 result := browseCollaboration(t, tenant, owner, "browseissues", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "limit": 2, "q": "security", "label": "security"})
167 items := result["items"].([]collaborationItem)
168 if len(items) != 1 || items[0].Status != "closed" {
169 t.Fatalf("search result = %#v", result)
170 }
171 filtered := browseCollaboration(t, tenant, owner, "browseissues", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "limit": 2, "state": "closed"})
172 if len(filtered["items"].([]collaborationItem)) != 1 {
173 t.Fatalf("status filter = %#v", filtered)
174 }
175}
176
177func TestCollaborationReplyPaginationUsesCursorAndFiltersMalformedEvents(t *testing.T) {
178 _, tenant := testTenant(t)
179 p := tenant.Policy()
180 p.Features.Grasp = true
181 p.Features.Grasp02 = true
182 p.Features.Grasp05 = true
183 p.GuestReplies = true
184 if err := tenant.applyPolicy(context.Background(), p); err != nil {
185 t.Fatal(err)
186 }
187 owner, coordinate := collaborationFixtureRepo(t, tenant)
188 authorSecret := strings.Repeat("8", 63) + "9"
189 author, err := event.PublicKey(authorSecret)
190 if err != nil {
191 t.Fatal(err)
192 }
193 issue := signedCollaborationEvent(t, authorSecret, event.KIND_GIT_ISSUE, 800,
194 [][]string{{"a", coordinate}, {"subject", "Crowded thread"}}, "body")
195 rows := []event.Event{issue}
196 for i := 0; i < 205; i++ {
197 rows = append(rows, signedCollaborationEvent(t, authorSecret, 1111, 801,
198 [][]string{{"E", issue.ID}, {"K", "1621"}, {"P", author}}, fmt.Sprintf("reply %03d", i)))
199 }
200 rows = append(rows, signedCollaborationEvent(t, authorSecret, 1111, 801,
201 [][]string{{"E", issue.ID}, {"K", "1618"}, {"P", author}}, "wrong root kind"))
202 rows = append(rows, signedCollaborationEvent(t, authorSecret, 1111, 801,
203 [][]string{{"E", issue.ID}, {"K", "1621"}, {"P", strings.Repeat("a", 64)}}, "wrong root author"))
204 saveCollaborationEvents(t, tenant, rows...)
205 seen := map[string]bool{}
206 cursor := ""
207 for page := 0; page < 20; page++ {
208 result := browseCollaborationDetail(t, tenant, owner, "browseissue", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "event": issue.ID, "limit": 37, "cursor": cursor})
209 if len(result.Replies) > 37 {
210 t.Fatalf("reply page has %d entries", len(result.Replies))
211 }
212 for _, reply := range result.Replies {
213 if seen[reply.ID] {
214 t.Fatalf("duplicate reply %s", reply.ID)
215 }
216 seen[reply.ID] = true
217 }
218 cursor = result.ReplyCursor
219 if cursor == "" {
220 break
221 }
222 }
223 if len(seen) != 205 {
224 t.Fatalf("reply pagination returned %d replies, want 205", len(seen))
225 }
226}
227
228func TestCollaborationDetailSeparatesRepliesAndAuthorizesStatus(t *testing.T) {
229 _, tenant := testTenant(t)
230 p := tenant.Policy()
231 p.Features.Grasp = true
232 p.Features.Grasp02 = true
233 p.Features.Grasp05 = true
234 p.GuestReplies = true
235 if err := tenant.applyPolicy(context.Background(), p); err != nil {
236 t.Fatal(err)
237 }
238 owner, coordinate := collaborationFixtureRepo(t, tenant)
239 authorSecret := strings.Repeat("3", 63) + "4"
240 author, err := event.PublicKey(authorSecret)
241 if err != nil {
242 t.Fatal(err)
243 }
244 issue := signedCollaborationEvent(t, authorSecret, event.KIND_GIT_ISSUE, 600,
245 [][]string{{"a", coordinate}, {"subject", "Needs review"}, {"t", "bug"}}, "Please investigate")
246 reply := signedCollaborationEvent(t, strings.Repeat("4", 63)+"5", 1111, 601,
247 [][]string{{"e", issue.ID}, {"E", issue.ID}, {"K", "1621"}, {"P", author}}, "I can reproduce this")
248 status := signedCollaborationEvent(t, authorSecret, 1631, 602, [][]string{{"e", issue.ID}}, "resolved")
249 forged := signedCollaborationEvent(t, strings.Repeat("5", 63)+"6", 1632, 603, [][]string{{"e", issue.ID}}, "closed")
250 saveCollaborationEvents(t, tenant, issue, reply, status, forged)
251 for _, actor := range []string{owner, author} {
252 result := browseCollaborationDetail(t, tenant, actor, "browseissue", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "event": issue.ID})
253 if !result.CanStatus {
254 t.Fatalf("actor %s cannot status: %#v", actor, result)
255 }
256 if len(result.Replies) != 1 || len(result.Statuses) != 1 {
257 t.Fatalf("authorized detail thread leaked or duplicated events: %#v", result)
258 }
259 }
260 unauthorized := strings.Repeat("6", 63) + "7"
261 unauthorizedKey, err := event.PublicKey(unauthorized)
262 if err != nil {
263 t.Fatal(err)
264 }
265 result := browseCollaborationDetail(t, tenant, unauthorizedKey, "browseissue", map[string]any{"owner": owner, "repo": collaborationCoordinateRepo, "event": issue.ID})
266 if result.CanStatus {
267 t.Fatalf("unauthorized actor can status: %#v", result)
268 }
269}
270
271func TestPrivateCollaborationRejectsAnonymousAndWrongRepository(t *testing.T) {
272 ctx := context.Background()
273 secret := strings.Repeat("7", 64)
274 owner, err := event.PublicKey(secret)
275 if err != nil {
276 t.Fatal(err)
277 }
278 _, tenant, server := privatePeerTenant(t, "collaboration-private", owner)
279 repo, _, _ := seedPrivateRepository(t, tenant, owner, secret, "private", server.URL)
280 coordinate := "30617:" + owner + ":" + repo.Identifier
281 issue := signedCollaborationEvent(t, secret, event.KIND_GIT_ISSUE, 700,
282 [][]string{{"a", coordinate}, {"subject", "Private issue"}}, "secret")
283 saveCollaborationEvents(t, tenant, issue)
284 params := []json.RawMessage{json.RawMessage(fmt.Sprintf(`{"owner":%q,"repo":%q,"event":%q}`, owner, repo.Identifier, issue.ID))}
285 if _, err := tenant.Execute(ctx, "", "browseissue", params); err == nil {
286 t.Fatal("anonymous private collaboration detail was exposed")
287 }
288 _, _, _ = seedPrivateRepository(t, tenant, owner, secret, "other", server.URL)
289 if _, err := tenant.Execute(ctx, owner, "browseissue", []json.RawMessage{json.RawMessage(fmt.Sprintf(`{"owner":%q,"repo":%q,"event":%q}`, owner, "other", issue.ID))}); err == nil {
290 t.Fatal("collaboration root crossed repository boundary")
291 }
292}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs