Monorepo for Tangled
tangled.org
1package knotserver
2
3import (
4 "context"
5 "io"
6 "log/slog"
7 "path/filepath"
8 "testing"
9
10 "github.com/bluesky-social/indigo/atproto/syntax"
11
12 "tangled.org/core/knotserver/db"
13 "tangled.org/core/rbac"
14)
15
16const (
17 bfOwner = "did:plc:akshay"
18 bfCollab = "did:plc:boltless"
19 bfRepo = "did:plc:limpet"
20)
21
22func newBackfillEnv(t *testing.T) (*db.DB, *rbac.Enforcer) {
23 t.Helper()
24 dir := t.TempDir()
25 d, err := db.Setup(context.Background(), filepath.Join(dir, "knot.db"))
26 if err != nil {
27 t.Fatalf("db.Setup: %v", err)
28 }
29 e, err := rbac.NewEnforcer(filepath.Join(dir, "rbac.db"))
30 if err != nil {
31 t.Fatalf("NewEnforcer: %v", err)
32 }
33 if err := e.AddKnot(rbac.ThisServer); err != nil {
34 t.Fatalf("AddKnot: %v", err)
35 }
36 if err := e.AddKnotOwner(rbac.ThisServer, bfOwner); err != nil {
37 t.Fatalf("AddKnotOwner: %v", err)
38 }
39 return d, e
40}
41
42func seedCasbinRepo(t *testing.T, d *db.DB, e *rbac.Enforcer, repoDid string, collaborators ...string) {
43 t.Helper()
44 if err := d.StoreRepoKey(repoDid, []byte("signing"), bfOwner, "reponame"); err != nil {
45 t.Fatalf("StoreRepoKey: %v", err)
46 }
47 if err := e.AddRepo(bfOwner, rbac.ThisServer, repoDid); err != nil {
48 t.Fatalf("AddRepo: %v", err)
49 }
50 for _, c := range collaborators {
51 if err := e.AddCollaborator(c, rbac.ThisServer, repoDid); err != nil {
52 t.Fatalf("AddCollaborator %s: %v", c, err)
53 }
54 }
55}
56
57func runBackfill(t *testing.T, d *db.DB, e *rbac.Enforcer) {
58 t.Helper()
59 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
60 if err := BackfillCollaborators(context.Background(), d, e, logger, true); err != nil {
61 t.Fatalf("BackfillCollaborators: %v", err)
62 }
63}
64
65func runMemberBackfill(t *testing.T, d *db.DB, e *rbac.Enforcer) {
66 t.Helper()
67 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
68 if err := BackfillKnotMembers(context.Background(), d, e, bfOwner, logger); err != nil {
69 t.Fatalf("BackfillKnotMembers: %v", err)
70 }
71}
72
73func TestBackfillCollaborators_FoldsCasbinAndExcludesOwner(t *testing.T) {
74 d, e := newBackfillEnv(t)
75 seedCasbinRepo(t, d, e, bfRepo, bfCollab)
76
77 runBackfill(t, d, e)
78
79 list, _, err := db.ListCollaborators(d, syntax.DID(bfRepo), db.ListPage{Limit: db.ListMaxLimit})
80 if err != nil {
81 t.Fatalf("ListCollaborators: %v", err)
82 }
83 if len(list) != 1 {
84 t.Fatalf("collaborators = %+v, want exactly one (owner must be excluded)", list)
85 }
86 if list[0].Subject != syntax.DID(bfCollab) {
87 t.Errorf("subject = %s, want %s", list[0].Subject, bfCollab)
88 }
89 if list[0].AddedBy != syntax.DID(bfOwner) {
90 t.Errorf("addedBy = %s, want owner %s", list[0].AddedBy, bfOwner)
91 }
92}
93
94func TestBackfillCollaborators_OneTimeAndNonDestructive(t *testing.T) {
95 d, e := newBackfillEnv(t)
96 seedCasbinRepo(t, d, e, bfRepo, bfCollab)
97
98 runBackfill(t, d, e)
99
100 if err := e.AddCollaborator("did:plc:scallop", rbac.ThisServer, bfRepo); err != nil {
101 t.Fatalf("post-migration casbin add: %v", err)
102 }
103 runBackfill(t, d, e)
104
105 list, _, err := db.ListCollaborators(d, syntax.DID(bfRepo), db.ListPage{Limit: db.ListMaxLimit})
106 if err != nil {
107 t.Fatalf("ListCollaborators: %v", err)
108 }
109 if len(list) != 1 {
110 t.Fatalf("collaborators = %d, want 1; backfill must run once and never resurrect later casbin state", len(list))
111 }
112 if list[0].Subject != syntax.DID(bfCollab) {
113 t.Errorf("subject = %s, want original %s preserved", list[0].Subject, bfCollab)
114 }
115}
116
117func TestBackfillCollaborators_LeavesMembersUntouched(t *testing.T) {
118 d, e := newBackfillEnv(t)
119 seedCasbinRepo(t, d, e, bfRepo, bfCollab)
120
121 owner := syntax.DID(bfOwner)
122 member := syntax.DID("did:plc:whelk")
123 if err := db.AddKnotMemberDirect(d, owner, member); err != nil {
124 t.Fatalf("seed member: %v", err)
125 }
126 if err := e.AddKnotMember(rbac.ThisServer, member.String()); err != nil {
127 t.Fatalf("seed member acl: %v", err)
128 }
129
130 runBackfill(t, d, e)
131
132 members, _, err := db.ListKnotMembers(d, db.ListPage{Limit: db.ListMaxLimit})
133 if err != nil {
134 t.Fatalf("ListKnotMembers: %v", err)
135 }
136 if len(members) != 1 || members[0].Subject != member {
137 t.Fatalf("members = %+v, want the seeded member preserved", members)
138 }
139
140 collabs, _, err := db.ListCollaborators(d, syntax.DID(bfRepo), db.ListPage{Limit: db.ListMaxLimit})
141 if err != nil {
142 t.Fatalf("ListCollaborators: %v", err)
143 }
144 if len(collabs) != 1 || collabs[0].Subject != syntax.DID(bfCollab) {
145 t.Errorf("collaborators = %+v, want only the casbin collaborator", collabs)
146 }
147}
148
149func TestBackfillCollaborators_UnmarkedRunDefersMarker(t *testing.T) {
150 d, e := newBackfillEnv(t)
151 seedCasbinRepo(t, d, e, bfRepo, bfCollab)
152
153 logger := slog.New(slog.NewTextHandler(io.Discard, nil))
154 if err := BackfillCollaborators(context.Background(), d, e, logger, false); err != nil {
155 t.Fatalf("unmarked backfill: %v", err)
156 }
157
158 list, _, err := db.ListCollaborators(d, syntax.DID(bfRepo), db.ListPage{Limit: db.ListMaxLimit})
159 if err != nil {
160 t.Fatalf("ListCollaborators: %v", err)
161 }
162 if len(list) != 1 {
163 t.Fatalf("collaborators = %d, want 1 after unmarked run", len(list))
164 }
165 applied, err := d.IsMigrationApplied(collaboratorBackfillMigration)
166 if err != nil {
167 t.Fatalf("IsMigrationApplied: %v", err)
168 }
169 if applied {
170 t.Fatal("unmarked run must not write the migration marker")
171 }
172
173 if err := e.AddCollaborator("did:plc:scallop", rbac.ThisServer, bfRepo); err != nil {
174 t.Fatalf("late casbin add: %v", err)
175 }
176 runBackfill(t, d, e)
177
178 list, _, err = db.ListCollaborators(d, syntax.DID(bfRepo), db.ListPage{Limit: db.ListMaxLimit})
179 if err != nil {
180 t.Fatalf("ListCollaborators after marked run: %v", err)
181 }
182 if len(list) != 2 {
183 t.Errorf("collaborators = %d, want 2; the marked rerun must fold late casbin state", len(list))
184 }
185 if applied, _ := d.IsMigrationApplied(collaboratorBackfillMigration); !applied {
186 t.Error("marked run did not write the migration marker")
187 }
188}
189
190func TestBackfillKnotMembers_FoldsCasbinAndExcludesOwner(t *testing.T) {
191 d, e := newBackfillEnv(t)
192 member := syntax.DID("did:plc:whelk")
193 if err := e.AddKnotMember(rbac.ThisServer, member.String()); err != nil {
194 t.Fatalf("seed casbin member: %v", err)
195 }
196
197 runMemberBackfill(t, d, e)
198
199 members, _, err := db.ListKnotMembers(d, db.ListPage{Limit: db.ListMaxLimit})
200 if err != nil {
201 t.Fatalf("ListKnotMembers: %v", err)
202 }
203 if len(members) != 1 {
204 t.Fatalf("members = %+v, want exactly one; the owner must be excluded", members)
205 }
206 if members[0].Subject != member {
207 t.Errorf("subject = %s, want %s", members[0].Subject, member)
208 }
209 if members[0].Did != syntax.DID(bfOwner) {
210 t.Errorf("added by = %s, want owner %s", members[0].Did, bfOwner)
211 }
212}
213
214func TestBackfillKnotMembers_OneTimeAndNonDestructive(t *testing.T) {
215 d, e := newBackfillEnv(t)
216 member := syntax.DID("did:plc:whelk")
217 if err := e.AddKnotMember(rbac.ThisServer, member.String()); err != nil {
218 t.Fatalf("seed casbin member: %v", err)
219 }
220
221 runMemberBackfill(t, d, e)
222
223 if err := e.AddKnotMember(rbac.ThisServer, "did:plc:scallop"); err != nil {
224 t.Fatalf("post-migration casbin add: %v", err)
225 }
226 runMemberBackfill(t, d, e)
227
228 members, _, err := db.ListKnotMembers(d, db.ListPage{Limit: db.ListMaxLimit})
229 if err != nil {
230 t.Fatalf("ListKnotMembers: %v", err)
231 }
232 if len(members) != 1 || members[0].Subject != member {
233 t.Fatalf("members = %+v, want only the original member; backfill must run once", members)
234 }
235 if applied, _ := d.IsMigrationApplied(knotMemberBackfillMigration); !applied {
236 t.Error("member backfill did not write its migration marker")
237 }
238}
239
240func TestBackfillCollaborators_EmptyMarksApplied(t *testing.T) {
241 d, e := newBackfillEnv(t)
242 seedCasbinRepo(t, d, e, bfRepo)
243
244 runBackfill(t, d, e)
245
246 applied, err := d.IsMigrationApplied(collaboratorBackfillMigration)
247 if err != nil {
248 t.Fatalf("IsMigrationApplied: %v", err)
249 }
250 if !applied {
251 t.Fatal("migration not marked applied after a zero-collaborator backfill; it would re-scan every boot")
252 }
253
254 if err := e.AddCollaborator(bfCollab, rbac.ThisServer, bfRepo); err != nil {
255 t.Fatalf("post-migration casbin add: %v", err)
256 }
257 runBackfill(t, d, e)
258
259 list, _, err := db.ListCollaborators(d, syntax.DID(bfRepo), db.ListPage{Limit: db.ListMaxLimit})
260 if err != nil {
261 t.Fatalf("ListCollaborators: %v", err)
262 }
263 if len(list) != 0 {
264 t.Errorf("collaborators = %d, want 0; an applied migration must not fold later casbin state", len(list))
265 }
266}