Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

knotserver/db: test direct member & collaborator ACL queries

Lewis: May this revision serve well! <lewis@tangled.org>

author
Lewis
committer
Tangled
date (Jun 16, 2026, 9:04 PM +0300) commit e6121a07 parent dcde7265 change-id sootnzzp
+272
+272
knotserver/db/direct_acl_test.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "path/filepath" 6 + "slices" 7 + "testing" 8 + 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + ) 11 + 12 + func newACLTestDB(t *testing.T) *DB { 13 + t.Helper() 14 + d, err := Setup(context.Background(), filepath.Join(t.TempDir(), "knot.db")) 15 + if err != nil { 16 + t.Fatalf("Setup: %v", err) 17 + } 18 + return d 19 + } 20 + 21 + func countMembers(t *testing.T, d *DB, where string, args ...any) int { 22 + t.Helper() 23 + var n int 24 + if err := d.QueryRow("select count(1) from knot_members where "+where, args...).Scan(&n); err != nil { 25 + t.Fatalf("count members: %v", err) 26 + } 27 + return n 28 + } 29 + 30 + func seedLegacyMember(t *testing.T, d *DB, owner, rkey, subject string) { 31 + t.Helper() 32 + if _, err := d.Exec( 33 + `insert into knot_members (did, rkey, subject) values (?, ?, ?)`, 34 + owner, rkey, subject, 35 + ); err != nil { 36 + t.Fatalf("seed legacy member: %v", err) 37 + } 38 + } 39 + 40 + func TestAddKnotMemberDirect_IdempotentUnderPartialUnique(t *testing.T) { 41 + d := newACLTestDB(t) 42 + owner := syntax.DID("did:plc:akshay") 43 + subject := syntax.DID("did:plc:boltless") 44 + 45 + if err := AddKnotMemberDirect(d, owner, subject); err != nil { 46 + t.Fatalf("first add: %v", err) 47 + } 48 + if err := AddKnotMemberDirect(d, owner, subject); err != nil { 49 + t.Fatalf("second add: %v", err) 50 + } 51 + 52 + if got := countMembers(t, d, "subject = ? and rkey is null", subject); got != 1 { 53 + t.Errorf("direct rows = %d, want 1", got) 54 + } 55 + } 56 + 57 + func TestRemoveKnotMemberDirect_PreservesLegacyRow(t *testing.T) { 58 + d := newACLTestDB(t) 59 + owner := syntax.DID("did:plc:akshay") 60 + subject := syntax.DID("did:plc:boltless") 61 + 62 + seedLegacyMember(t, d, owner.String(), "legacy-rk", subject.String()) 63 + if err := AddKnotMemberDirect(d, owner, subject); err != nil { 64 + t.Fatalf("direct add: %v", err) 65 + } 66 + if got := countMembers(t, d, "subject = ?", subject.String()); got != 2 { 67 + t.Fatalf("rows = %d, want 2 legacy plus direct", got) 68 + } 69 + 70 + if err := RemoveKnotMemberDirect(d, subject); err != nil { 71 + t.Fatalf("remove direct: %v", err) 72 + } 73 + if got := countMembers(t, d, "subject = ? and rkey is null", subject.String()); got != 0 { 74 + t.Errorf("direct rows after remove = %d, want 0", got) 75 + } 76 + if got := countMembers(t, d, "subject = ? and rkey = 'legacy-rk'", subject.String()); got != 1 { 77 + t.Errorf("legacy row = %d, want 1 preserved", got) 78 + } 79 + } 80 + 81 + func TestRemoveKnotMemberBySubject_RemovesAllRows(t *testing.T) { 82 + d := newACLTestDB(t) 83 + owner := syntax.DID("did:plc:akshay") 84 + subject := syntax.DID("did:plc:boltless") 85 + 86 + seedLegacyMember(t, d, owner.String(), "legacy-rk", subject.String()) 87 + if err := AddKnotMemberDirect(d, owner, subject); err != nil { 88 + t.Fatalf("direct add: %v", err) 89 + } 90 + 91 + if err := RemoveKnotMemberBySubject(d, subject); err != nil { 92 + t.Fatalf("remove by subject: %v", err) 93 + } 94 + if got := countMembers(t, d, "subject = ?", subject.String()); got != 0 { 95 + t.Errorf("rows after remove = %d, want 0", got) 96 + } 97 + } 98 + 99 + func TestCollaborators_AddListRemoveScopedByRepo(t *testing.T) { 100 + d := newACLTestDB(t) 101 + owner := syntax.DID("did:plc:akshay") 102 + subject := syntax.DID("did:plc:boltless") 103 + repoA := syntax.DID("did:plc:limpet") 104 + repoB := syntax.DID("did:plc:scallop") 105 + 106 + for _, repo := range []syntax.DID{repoA, repoB} { 107 + if err := AddCollaborator(d, Collaborator{RepoDid: repo, Subject: subject, AddedBy: owner}); err != nil { 108 + t.Fatalf("add collaborator on %s: %v", repo, err) 109 + } 110 + } 111 + 112 + listA, _, err := ListCollaborators(d, repoA, ListPage{Limit: ListMaxLimit}) 113 + if err != nil { 114 + t.Fatalf("list A: %v", err) 115 + } 116 + if len(listA) != 1 || listA[0].Subject != subject || listA[0].RepoDid != repoA { 117 + t.Fatalf("repoA collaborators = %+v, want one boltless on limpet", listA) 118 + } 119 + 120 + if err := RemoveCollaborator(d, repoA, subject); err != nil { 121 + t.Fatalf("remove on A: %v", err) 122 + } 123 + if listA, _, _ = ListCollaborators(d, repoA, ListPage{Limit: ListMaxLimit}); len(listA) != 0 { 124 + t.Errorf("repoA after remove = %d, want 0", len(listA)) 125 + } 126 + if listB, _, _ := ListCollaborators(d, repoB, ListPage{Limit: ListMaxLimit}); len(listB) != 1 { 127 + t.Errorf("repoB after removing from A = %d, want 1 scoped", len(listB)) 128 + } 129 + } 130 + 131 + func TestAddCollaborator_IdempotentUnderUnique(t *testing.T) { 132 + d := newACLTestDB(t) 133 + owner := syntax.DID("did:plc:akshay") 134 + subject := syntax.DID("did:plc:boltless") 135 + repo := syntax.DID("did:plc:limpet") 136 + 137 + if err := AddCollaborator(d, Collaborator{RepoDid: repo, Subject: subject, AddedBy: owner}); err != nil { 138 + t.Fatalf("first add: %v", err) 139 + } 140 + if err := AddCollaborator(d, Collaborator{RepoDid: repo, Subject: subject, AddedBy: owner}); err != nil { 141 + t.Fatalf("second add: %v", err) 142 + } 143 + 144 + list, _, err := ListCollaborators(d, repo, ListPage{Limit: ListMaxLimit}) 145 + if err != nil { 146 + t.Fatalf("list: %v", err) 147 + } 148 + if len(list) != 1 { 149 + t.Errorf("rows = %d, want 1", len(list)) 150 + } 151 + } 152 + 153 + func TestListKnotMembers_DedupsLegacyAndDirect(t *testing.T) { 154 + d := newACLTestDB(t) 155 + owner := syntax.DID("did:plc:akshay") 156 + subject := syntax.DID("did:plc:boltless") 157 + 158 + seedLegacyMember(t, d, owner.String(), "legacy-rk", subject.String()) 159 + if err := AddKnotMemberDirect(d, owner, subject); err != nil { 160 + t.Fatalf("direct add: %v", err) 161 + } 162 + if got := countMembers(t, d, "subject = ?", subject.String()); got != 2 { 163 + t.Fatalf("raw rows = %d, want 2 (legacy + direct)", got) 164 + } 165 + 166 + members, next, err := ListKnotMembers(d, ListPage{Limit: ListMaxLimit}) 167 + if err != nil { 168 + t.Fatalf("list: %v", err) 169 + } 170 + if len(members) != 1 { 171 + t.Fatalf("members = %d, want 1; legacy and direct rows for one subject must collapse", len(members)) 172 + } 173 + if members[0].Subject != subject { 174 + t.Errorf("subject = %s, want %s", members[0].Subject, subject) 175 + } 176 + if next != nil { 177 + t.Errorf("cursor = %v, want nil for a complete page", *next) 178 + } 179 + } 180 + 181 + func TestListKnotMembers_OrderAndKeyset(t *testing.T) { 182 + d := newACLTestDB(t) 183 + owner := syntax.DID("did:plc:akshay") 184 + for _, s := range []string{"did:plc:limpet", "did:plc:whelk", "did:plc:scallop"} { 185 + if err := AddKnotMemberDirect(d, owner, syntax.DID(s)); err != nil { 186 + t.Fatalf("add %s: %v", s, err) 187 + } 188 + } 189 + 190 + asc, _, err := ListKnotMembers(d, ListPage{Limit: ListMaxLimit, Desc: false}) 191 + if err != nil { 192 + t.Fatalf("asc: %v", err) 193 + } 194 + if !slices.IsSortedFunc(asc, func(a, b KnotMember) int { return a.Id - b.Id }) { 195 + t.Errorf("asc not ascending by id: %+v", asc) 196 + } 197 + 198 + desc, _, err := ListKnotMembers(d, ListPage{Limit: ListMaxLimit, Desc: true}) 199 + if err != nil { 200 + t.Fatalf("desc: %v", err) 201 + } 202 + if !slices.IsSortedFunc(desc, func(a, b KnotMember) int { return b.Id - a.Id }) { 203 + t.Errorf("desc not descending by id: %+v", desc) 204 + } 205 + 206 + page1, next, err := ListKnotMembers(d, ListPage{Limit: 2, Desc: false}) 207 + if err != nil { 208 + t.Fatalf("page1: %v", err) 209 + } 210 + if len(page1) != 2 || next == nil { 211 + t.Fatalf("page1 = %d next=%v, want 2 rows and a cursor", len(page1), next) 212 + } 213 + page2, next2, err := ListKnotMembers(d, ListPage{Limit: 2, Cursor: next, Desc: false}) 214 + if err != nil { 215 + t.Fatalf("page2: %v", err) 216 + } 217 + if len(page2) != 1 || next2 != nil { 218 + t.Fatalf("page2 = %d next=%v, want 1 row and no cursor", len(page2), next2) 219 + } 220 + if page1[len(page1)-1].Id >= page2[0].Id { 221 + t.Errorf("keyset overlap or gap: page1 last id %d, page2 first id %d", page1[len(page1)-1].Id, page2[0].Id) 222 + } 223 + } 224 + 225 + func TestListKnotMembers_ZeroValuePageDefaultsLimit(t *testing.T) { 226 + d := newACLTestDB(t) 227 + owner := syntax.DID("did:plc:akshay") 228 + for _, s := range []string{"did:plc:limpet", "did:plc:whelk"} { 229 + if err := AddKnotMemberDirect(d, owner, syntax.DID(s)); err != nil { 230 + t.Fatalf("add %s: %v", s, err) 231 + } 232 + } 233 + 234 + for _, p := range []ListPage{{}, {Limit: -3}} { 235 + members, next, err := ListKnotMembers(d, p) 236 + if err != nil { 237 + t.Fatalf("list with page %+v: %v", p, err) 238 + } 239 + if len(members) != 2 { 240 + t.Errorf("page %+v: members = %d, want 2 under the default limit", p, len(members)) 241 + } 242 + if next != nil { 243 + t.Errorf("page %+v: cursor = %v, want nil", p, *next) 244 + } 245 + } 246 + } 247 + 248 + func TestDeleteRepoKeyRemovesCollaborators(t *testing.T) { 249 + d := newACLTestDB(t) 250 + 251 + repoDid := syntax.DID("did:plc:whelk") 252 + owner := syntax.DID("did:plc:akshay") 253 + subject := syntax.DID("did:plc:boltless") 254 + 255 + if err := d.StoreRepoKey(repoDid.String(), []byte("signing"), owner.String(), "reponame"); err != nil { 256 + t.Fatalf("StoreRepoKey: %v", err) 257 + } 258 + if err := AddCollaborator(d, Collaborator{RepoDid: repoDid, Subject: subject, AddedBy: owner}); err != nil { 259 + t.Fatalf("AddCollaborator: %v", err) 260 + } 261 + if ok, err := IsCollaborator(d, repoDid, subject); err != nil || !ok { 262 + t.Fatalf("collaborator missing before delete: ok=%v err=%v", ok, err) 263 + } 264 + 265 + if err := d.DeleteRepoKey(repoDid.String()); err != nil { 266 + t.Fatalf("DeleteRepoKey: %v", err) 267 + } 268 + 269 + if ok, err := IsCollaborator(d, repoDid, subject); err != nil || ok { 270 + t.Fatalf("collaborator not removed after repo delete: ok=%v err=%v", ok, err) 271 + } 272 + }