This repository has no description
0

Configure Feed

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

core / spindle / ingester_test.go
8.3 kB 293 lines
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}