This repository has no description
1package spindle
2
3import (
4 "context"
5 "encoding/json"
6 "log/slog"
7 "strings"
8 "tangled.org/core/jetstream"
9 "testing"
10
11 "github.com/bluesky-social/indigo/atproto/syntax"
12 "github.com/bluesky-social/jetstream/pkg/models"
13
14 "tangled.org/core/api/tangled"
15 "tangled.org/core/rbac"
16 "tangled.org/core/spindle/config"
17 "tangled.org/core/tapc"
18)
19
20func TestTapProcessEventIgnoresPullRecords(t *testing.T) {
21 client := &Tap{}
22 err := client.processEvent(context.Background(), tapc.Event{
23 Type: tapc.EvtRecord,
24 Record: &tapc.RecordEventData{
25 Live: true,
26 Did: syntax.DID("did:plc:jge3zxi7lgrfnvhzcgrimeo7"),
27 Collection: syntax.NSID(tangled.RepoPullNSID),
28 Rkey: syntax.RecordKey("3mrhpypucbsg4"),
29 Action: tapc.RecordCreateAction,
30 Record: json.RawMessage(`{`),
31 },
32 })
33 if err != nil {
34 t.Fatalf("Tap.processEvent() returned an error for a pull record: %v", err)
35 }
36}
37
38func TestJetstreamToTapEventMarksPullRecordsLive(t *testing.T) {
39 tests := []struct {
40 name string
41 operation string
42 action tapc.RecordAction
43 }{
44 {name: "create", operation: models.CommitOperationCreate, action: tapc.RecordCreateAction},
45 {name: "update", operation: models.CommitOperationUpdate, action: tapc.RecordUpdateAction},
46 {name: "delete", operation: models.CommitOperationDelete, action: tapc.RecordDeleteAction},
47 }
48
49 for _, tt := range tests {
50 t.Run(tt.name, func(t *testing.T) {
51 event, ok := jetstreamToTapEvent(&models.Event{
52 Did: "did:plc:jge3zxi7lgrfnvhzcgrimeo7",
53 Kind: models.EventKindCommit,
54 Commit: &models.Commit{
55 Operation: tt.operation,
56 Collection: tangled.RepoPullNSID,
57 RKey: "3mrhpypucbsg4",
58 Record: json.RawMessage(`{"title":"test"}`),
59 },
60 })
61 if !ok {
62 t.Fatal("jetstreamToTapEvent() rejected a valid pull event")
63 }
64 if event.Record == nil {
65 t.Fatal("jetstreamToTapEvent() returned no record")
66 }
67 if !event.Record.Live {
68 t.Error("converted pull event is not live")
69 }
70 if event.Record.Collection.String() != tangled.RepoPullNSID {
71 t.Errorf("collection = %q, want %q", event.Record.Collection, tangled.RepoPullNSID)
72 }
73 if event.Record.Action != tt.action {
74 t.Errorf("action = %q, want %q", event.Record.Action, tt.action)
75 }
76 })
77 }
78}
79
80func TestEmbeddedTapDoesNotSubscribeToPullRecords(t *testing.T) {
81 tcfg := newEmbeddedTapConfig(&config.Config{})
82
83 if tcfg.SignalCollection == tangled.RepoPullNSID {
84 t.Errorf("SignalCollection = %q, must not ingest pull records", tcfg.SignalCollection)
85 }
86 for _, collection := range tcfg.CollectionFilters {
87 if collection == tangled.RepoPullNSID {
88 t.Errorf("CollectionFilters includes %q", tangled.RepoPullNSID)
89 }
90 }
91}
92
93func TestIngestMember_RBAC(t *testing.T) {
94 d, e := newTestSpindleDB(t)
95
96 cfg := &config.Config{}
97 cfg.Server.Hostname = "spindle.test"
98
99 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false)
100 if jcerr != nil {
101 t.Fatalf("NewJetstreamClient: %v", jcerr)
102 }
103
104 s := &Spindle{
105 db: d,
106 e: e,
107 l: slog.Default(),
108 cfg: cfg,
109 jc: jc,
110 rootCtx: context.Background(),
111 }
112
113 actorDid := "did:plc:adminactor"
114 subjectDid := "did:plc:newmember"
115 rbacDomain := rbac.ThisServer
116
117 memberRecord := tangled.SpindleMember{
118 Instance: "spindle.test",
119 Subject: subjectDid,
120 }
121 memberRecordJson, _ := json.Marshal(memberRecord)
122
123 evt := &models.Event{
124 Did: actorDid,
125 Kind: models.EventKindCommit,
126 Commit: &models.Commit{
127 Operation: models.CommitOperationCreate,
128 Collection: tangled.SpindleMemberNSID,
129 RKey: "member-rkey-1",
130 Record: memberRecordJson,
131 },
132 }
133
134 err := s.ingestMember(context.Background(), evt)
135 if err == nil {
136 t.Fatal("expected permission denied error, got nil")
137 }
138 if !strings.Contains(err.Error(), "permission denied") {
139 t.Fatalf("expected permission denied, got error: %v", err)
140 }
141
142 var dbCount int
143 err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount)
144 if err != nil {
145 t.Fatalf("DB query error: %v", err)
146 }
147 if dbCount > 0 {
148 t.Fatal("spindle member was registered in DB on failed auth")
149 }
150
151 err = e.AddSpindle(rbacDomain)
152 if err != nil {
153 t.Fatalf("AddSpindle: %v", err)
154 }
155 err = e.AddSpindleOwner(rbacDomain, actorDid)
156 if err != nil {
157 t.Fatalf("AddSpindleOwner: %v", err)
158 }
159
160 err = s.ingestMember(context.Background(), evt)
161 if err != nil {
162 t.Fatalf("ingestMember failed for authorized actor: %v", err)
163 }
164
165 err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount)
166 if err != nil || dbCount != 1 {
167 t.Fatalf("expected exactly 1 member in DB, got: %d (err: %v)", dbCount, err)
168 }
169
170 isMember, err := e.IsSpindleMember(subjectDid, rbacDomain)
171 if err != nil || !isMember {
172 t.Fatalf("expected subject to be spindle member in Casbin, got: %t (err: %v)", isMember, err)
173 }
174
175 deleteEvt := &models.Event{
176 Did: actorDid,
177 Kind: models.EventKindCommit,
178 Commit: &models.Commit{
179 Operation: models.CommitOperationDelete,
180 Collection: tangled.SpindleMemberNSID,
181 RKey: "member-rkey-1",
182 },
183 }
184
185 err = s.ingestMember(context.Background(), deleteEvt)
186 if err != nil {
187 t.Fatalf("ingestMember delete failed: %v", err)
188 }
189
190 err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount)
191 if err != nil || dbCount != 0 {
192 t.Fatalf("expected 0 members in DB after delete, got: %d (err: %v)", dbCount, err)
193 }
194
195 isMember, err = e.IsSpindleMember(subjectDid, rbacDomain)
196 if err != nil || isMember {
197 t.Fatalf("expected subject to NOT be spindle member in Casbin, got: %t (err: %v)", isMember, err)
198 }
199}
200
201func TestIngestMember_ForgeDeleteRejection(t *testing.T) {
202 d, e := newTestSpindleDB(t)
203
204 cfg := &config.Config{}
205 cfg.Server.Hostname = "spindle.test"
206
207 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false)
208 if jcerr != nil {
209 t.Fatalf("NewJetstreamClient: %v", jcerr)
210 }
211
212 s := &Spindle{
213 db: d,
214 e: e,
215 l: slog.Default(),
216 cfg: cfg,
217 jc: jc,
218 rootCtx: context.Background(),
219 }
220
221 adminDid := "did:plc:adminactor"
222 bobDid := "did:plc:bobactor"
223 subjectDid := "did:plc:newmember"
224 rbacDomain := rbac.ThisServer
225
226 err := e.AddSpindle(rbacDomain)
227 if err != nil {
228 t.Fatalf("AddSpindle: %v", err)
229 }
230 err = e.AddSpindleOwner(rbacDomain, adminDid)
231 if err != nil {
232 t.Fatalf("AddSpindleOwner: %v", err)
233 }
234
235 memberRecord := tangled.SpindleMember{
236 Instance: "spindle.test",
237 Subject: subjectDid,
238 }
239 memberRecordJson, _ := json.Marshal(memberRecord)
240
241 evt := &models.Event{
242 Did: adminDid,
243 Kind: models.EventKindCommit,
244 Commit: &models.Commit{
245 Operation: models.CommitOperationCreate,
246 Collection: tangled.SpindleMemberNSID,
247 RKey: "member-rkey-1",
248 Record: memberRecordJson,
249 },
250 }
251
252 err = s.ingestMember(context.Background(), evt)
253 if err != nil {
254 t.Fatalf("ingestMember failed for admin: %v", err)
255 }
256
257 var dbCount int
258 err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount)
259 if err != nil || dbCount != 1 {
260 t.Fatalf("expected member in DB, got: %d (err: %v)", dbCount, err)
261 }
262
263 isMember, err := e.IsSpindleMember(subjectDid, rbacDomain)
264 if err != nil || !isMember {
265 t.Fatalf("expected subject to be spindle member, got %t (err: %v)", isMember, err)
266 }
267
268 // bob tries to delete alice's spindle member record, must reject forged delete
269 deleteEvt := &models.Event{
270 Did: bobDid, // Bob is the actor
271 Kind: models.EventKindCommit,
272 Commit: &models.Commit{
273 Operation: models.CommitOperationDelete,
274 Collection: tangled.SpindleMemberNSID,
275 RKey: "member-rkey-1",
276 },
277 }
278
279 err = s.ingestMember(context.Background(), deleteEvt)
280 if err != nil {
281 t.Fatalf("ingestMember delete returned error: %v", err)
282 }
283
284 err = d.QueryRow(`select count(*) from spindle_members where subject = ?`, subjectDid).Scan(&dbCount)
285 if err != nil || dbCount != 1 {
286 t.Fatalf("member was deleted from DB, expected remaining, count: %d (err: %v)", dbCount, err)
287 }
288
289 isMember, err = e.IsSpindleMember(subjectDid, rbacDomain)
290 if err != nil || !isMember {
291 t.Fatal("member policy was removed from Casbin by forged delete")
292 }
293}