Monorepo for Tangled tangled.org
3

Configure Feed

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

core / spindle / tapclient_test.go
18 kB 694 lines
1package spindle 2 3import ( 4 "context" 5 "encoding/json" 6 "log/slog" 7 "strings" 8 "tangled.org/core/jetstream" 9 "testing" 10 "time" 11 12 "github.com/bluesky-social/indigo/atproto/identity" 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 "tangled.org/core/api/tangled" 15 "tangled.org/core/eventconsumer" 16 "tangled.org/core/idresolver" 17 "tangled.org/core/rbac" 18 "tangled.org/core/spindle/config" 19 "tangled.org/core/spindle/db" 20 21 "tangled.org/core/tapc" 22) 23 24type mockDirectory struct { 25 ident *identity.Identity 26} 27 28func (m *mockDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { 29 return m.ident, nil 30} 31 32func (m *mockDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { 33 return m.ident, nil 34} 35 36func (m *mockDirectory) Lookup(ctx context.Context, id syntax.AtIdentifier) (*identity.Identity, error) { 37 return m.ident, nil 38} 39 40func (m *mockDirectory) Purge(ctx context.Context, id syntax.AtIdentifier) error { 41 return nil 42} 43 44func TestProcessRepo_MembershipCheck(t *testing.T) { 45 d, e := newTestSpindleDB(t) 46 47 cfg := &config.Config{} 48 cfg.Server.Hostname = "spindle.test" 49 50 ccfg := eventconsumer.NewConsumerConfig() 51 ccfg.Logger = slog.Default() 52 ks := eventconsumer.NewConsumer(*ccfg) 53 54 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 55 if jcerr != nil { 56 t.Fatalf("NewJetstreamClient: %v", jcerr) 57 } 58 s := &Spindle{ 59 db: d, 60 e: e, 61 l: slog.Default(), 62 cfg: cfg, 63 ks: ks, 64 jc: jc, 65 rootCtx: context.Background(), 66 } 67 68 tap := &Tap{ 69 spindle: s, 70 logger: slog.Default(), 71 } 72 73 ownerDid := syntax.DID("did:plc:memberowner") 74 nonMemberDid := syntax.DID("did:plc:nonmemberowner") 75 repoDid := "did:plc:testrepo123" 76 77 err := e.AddSpindle(rbac.ThisServer) 78 if err != nil { 79 t.Fatalf("AddSpindle: %v", err) 80 } 81 err = e.AddSpindleMember(rbac.ThisServer, ownerDid.String()) 82 if err != nil { 83 t.Fatalf("AddSpindleMember: %v", err) 84 } 85 86 recNonMember := tangled.Repo{ 87 Knot: "knot.test", 88 RepoDid: &repoDid, 89 Spindle: &cfg.Server.Hostname, 90 CreatedAt: time.Now().Format(time.RFC3339), 91 } 92 recNonMemberJson, _ := json.Marshal(recNonMember) 93 94 err = tap.processRepo(context.Background(), &tapc.RecordEventData{ 95 Live: true, 96 Did: nonMemberDid, 97 Rkey: "test-repo-rkey", 98 Collection: syntax.NSID(tangled.RepoNSID), 99 Action: tapc.RecordCreateAction, 100 Record: recNonMemberJson, 101 }) 102 if err != nil { 103 t.Fatalf("processRepo returned error for non-member: %v", err) 104 } 105 106 _, err = d.GetRepoByOwnerRkey(nonMemberDid, "test-repo-rkey") 107 if err == nil { 108 t.Fatal("repo for non-member was registered in DB, expected rejection") 109 } 110 111 recMember := tangled.Repo{ 112 Knot: "knot.test", 113 RepoDid: &repoDid, 114 Spindle: &cfg.Server.Hostname, 115 CreatedAt: time.Now().Format(time.RFC3339), 116 } 117 recMemberJson, _ := json.Marshal(recMember) 118 119 err = tap.processRepo(context.Background(), &tapc.RecordEventData{ 120 Live: true, 121 Did: ownerDid, 122 Rkey: "test-repo-rkey", 123 Collection: syntax.NSID(tangled.RepoNSID), 124 Action: tapc.RecordCreateAction, 125 Record: recMemberJson, 126 }) 127 if err == nil { 128 t.Fatal("expected git clone error for valid member, but got nil") 129 } 130 131 if !strings.Contains(err.Error(), "setting up sparse-clone git repo") { 132 t.Fatalf("expected sparse-clone error, got: %v", err) 133 } 134} 135 136func TestProcessPull_PushAllowedCheck(t *testing.T) { 137 d, e := newTestSpindleDB(t) 138 139 cfg := &config.Config{} 140 cfg.Server.Hostname = "spindle.test" 141 142 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 143 if jcerr != nil { 144 t.Fatalf("NewJetstreamClient: %v", jcerr) 145 } 146 s := &Spindle{ 147 db: d, 148 e: e, 149 l: slog.Default(), 150 cfg: cfg, 151 res: idresolver.DefaultResolver("https://plc.test"), 152 jc: jc, 153 rootCtx: context.Background(), 154 } 155 156 repoOwnerDid := syntax.DID("did:plc:repoowner") 157 nonPusherDid := syntax.DID("did:plc:nonpusher") 158 pusherDid := syntax.DID("did:plc:pusher") 159 repoDid := syntax.DID("did:plc:testrepo123") 160 161 err := d.AddRepo(db.Repo{ 162 Knot: "knot.test", 163 Owner: repoOwnerDid, 164 Rkey: "test-repo-rkey", 165 RepoDid: repoDid, 166 CreatedAt: time.Now().Format(time.RFC3339), 167 }) 168 if err != nil { 169 t.Fatalf("AddRepo: %v", err) 170 } 171 172 err = e.AddRepo(repoOwnerDid.String(), rbac.ThisServer, repoDid.String()) 173 if err != nil { 174 t.Fatalf("AddRepo permissions: %v", err) 175 } 176 err = e.AddCollaborator(pusherDid.String(), rbac.ThisServer, repoDid.String()) 177 if err != nil { 178 t.Fatalf("AddCollaborator: %v", err) 179 } 180 181 pullRecord := tangled.RepoPull{ 182 Target: &tangled.RepoPull_Target{ 183 Branch: "main", 184 Repo: repoDid.String(), 185 }, 186 Source: &tangled.RepoPull_Source{ 187 Branch: "feature", 188 Repo: nil, // branch-based PR (source repo is nil) 189 }, 190 } 191 pullRecordJson, _ := json.Marshal(pullRecord) 192 193 err = s.processPull(context.Background(), &tapc.RecordEventData{ 194 Live: true, 195 Did: nonPusherDid, 196 Rkey: "pull-rkey-1", 197 Collection: syntax.NSID(tangled.RepoPullNSID), 198 Action: tapc.RecordCreateAction, 199 Record: pullRecordJson, 200 }) 201 if err != nil { 202 t.Fatalf("processPull returned error for non-pusher: %v", err) 203 } 204 205 // fetch fails because plc/pds are not real 206 err = s.processPull(context.Background(), &tapc.RecordEventData{ 207 Live: true, 208 Did: pusherDid, 209 Rkey: "pull-rkey-2", 210 Collection: syntax.NSID(tangled.RepoPullNSID), 211 Action: tapc.RecordCreateAction, 212 Record: pullRecordJson, 213 }) 214 if err == nil { 215 t.Fatal("expected error from fetchLatestSubmission for valid pusher, but got nil") 216 } 217 218 if !strings.Contains(err.Error(), "checking push access") && !strings.Contains(err.Error(), "resolve PR owner") && !strings.Contains(err.Error(), "invalid memory address") { 219 t.Fatalf("expected failed identity resolution or connection error, got: %v", err) 220 } 221} 222 223func TestProcessRepo_HijackRepoDidCheck(t *testing.T) { 224 d, e := newTestSpindleDB(t) 225 226 cfg := &config.Config{} 227 cfg.Server.Hostname = "spindle.test" 228 229 ccfg := eventconsumer.NewConsumerConfig() 230 ccfg.Logger = slog.Default() 231 ks := eventconsumer.NewConsumer(*ccfg) 232 233 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 234 if jcerr != nil { 235 t.Fatalf("NewJetstreamClient: %v", jcerr) 236 } 237 s := &Spindle{ 238 db: d, 239 e: e, 240 l: slog.Default(), 241 cfg: cfg, 242 ks: ks, 243 jc: jc, 244 rootCtx: context.Background(), 245 } 246 247 tap := &Tap{ 248 spindle: s, 249 logger: slog.Default(), 250 } 251 252 aliceDid := syntax.DID("did:plc:alice") 253 bobDid := syntax.DID("did:plc:bob") 254 repoDid := "did:plc:sharedrepo" 255 256 err := e.AddSpindle(rbac.ThisServer) 257 if err != nil { 258 t.Fatalf("AddSpindle: %v", err) 259 } 260 err = e.AddSpindleMember(rbac.ThisServer, aliceDid.String()) 261 if err != nil { 262 t.Fatalf("AddSpindleMember alice: %v", err) 263 } 264 err = e.AddSpindleMember(rbac.ThisServer, bobDid.String()) 265 if err != nil { 266 t.Fatalf("AddSpindleMember bob: %v", err) 267 } 268 269 err = d.AddRepo(db.Repo{ 270 Knot: "knot.test", 271 Owner: aliceDid, 272 Rkey: "alice-repo", 273 RepoDid: syntax.DID(repoDid), 274 CreatedAt: time.Now().Format(time.RFC3339), 275 }) 276 if err != nil { 277 t.Fatalf("d.AddRepo: %v", err) 278 } 279 280 // bob tries to register alice's repo did, must reject the hijack 281 recBob := tangled.Repo{ 282 Knot: "knot.test", 283 RepoDid: &repoDid, 284 Spindle: &cfg.Server.Hostname, 285 CreatedAt: time.Now().Format(time.RFC3339), 286 } 287 recBobJson, _ := json.Marshal(recBob) 288 289 err = tap.processRepo(context.Background(), &tapc.RecordEventData{ 290 Live: true, 291 Did: bobDid, 292 Rkey: "bob-repo", 293 Collection: syntax.NSID(tangled.RepoNSID), 294 Action: tapc.RecordCreateAction, 295 Record: recBobJson, 296 }) 297 if err != nil { 298 t.Fatalf("processRepo returned error on duplicate repoDid hijack attempt: %v", err) 299 } 300 301 _, err = d.GetRepoByOwnerRkey(bobDid, "bob-repo") 302 if err == nil { 303 t.Fatal("bob successfully hijacked alice's repoDid in DB, expected rejection") 304 } 305} 306 307func TestProcessCollaborator_RBAC(t *testing.T) { 308 d, e := newTestSpindleDB(t) 309 310 cfg := &config.Config{} 311 cfg.Server.Hostname = "spindle.test" 312 313 ownerDid := syntax.DID("did:plc:repoowner") 314 otherDid := syntax.DID("did:plc:otheractor") 315 subjectDid := syntax.DID("did:plc:collabsubject") 316 repoDid := syntax.DID("did:plc:testrepo123") 317 318 h, err := syntax.ParseHandle("collabsubject.test") 319 if err != nil { 320 t.Fatalf("syntax.ParseHandle: %v", err) 321 } 322 mockIdent := &identity.Identity{ 323 DID: subjectDid, 324 Handle: h, 325 } 326 resolver := idresolver.NewMockResolver(&mockDirectory{ident: mockIdent}) 327 328 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 329 if jcerr != nil { 330 t.Fatalf("NewJetstreamClient: %v", jcerr) 331 } 332 s := &Spindle{ 333 db: d, 334 e: e, 335 l: slog.Default(), 336 cfg: cfg, 337 res: resolver, 338 jc: jc, 339 rootCtx: context.Background(), 340 } 341 342 tap := &Tap{ 343 spindle: s, 344 logger: slog.Default(), 345 } 346 347 err = d.AddRepo(db.Repo{ 348 Knot: "knot.test", 349 Owner: ownerDid, 350 Rkey: "test-repo-rkey", 351 RepoDid: repoDid, 352 CreatedAt: time.Now().Format(time.RFC3339), 353 }) 354 if err != nil { 355 t.Fatalf("AddRepo: %v", err) 356 } 357 358 collabRecord := tangled.RepoCollaborator{ 359 Subject: subjectDid.String(), 360 Repo: repoDid.String(), 361 } 362 collabRecordJson, _ := json.Marshal(collabRecord) 363 364 err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ 365 Live: true, 366 Did: otherDid, 367 Rkey: "collab-rkey-1", 368 Collection: syntax.NSID(tangled.RepoCollaboratorNSID), 369 Action: tapc.RecordCreateAction, 370 Record: collabRecordJson, 371 }) 372 if err != nil { 373 t.Fatalf("processCollaborator returned error: %v", err) 374 } 375 376 _, err = d.GetRepoCollaborator(otherDid, "collab-rkey-1") 377 if err == nil { 378 t.Fatal("collaborator from non-owner was registered in DB") 379 } 380 381 err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ 382 Live: true, 383 Did: ownerDid, 384 Rkey: "collab-rkey-2", 385 Collection: syntax.NSID(tangled.RepoCollaboratorNSID), 386 Action: tapc.RecordCreateAction, 387 Record: collabRecordJson, 388 }) 389 if err != nil { 390 t.Fatalf("processCollaborator returned error: %v", err) 391 } 392 _, err = d.GetRepoCollaborator(ownerDid, "collab-rkey-2") 393 if err == nil { 394 t.Fatal("collaborator registered despite missing Casbin invite permission") 395 } 396 397 err = e.AddRepo(ownerDid.String(), rbac.ThisServer, repoDid.String()) 398 if err != nil { 399 t.Fatalf("AddRepo permissions: %v", err) 400 } 401 402 err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ 403 Live: true, 404 Did: ownerDid, 405 Rkey: "collab-rkey-3", 406 Collection: syntax.NSID(tangled.RepoCollaboratorNSID), 407 Action: tapc.RecordCreateAction, 408 Record: collabRecordJson, 409 }) 410 if err != nil { 411 t.Fatalf("processCollaborator failed for authorized owner: %v", err) 412 } 413 414 c, err := d.GetRepoCollaborator(ownerDid, "collab-rkey-3") 415 if err != nil { 416 t.Fatalf("GetRepoCollaborator error: %v", err) 417 } 418 if c.Subject != subjectDid || c.RepoDid != repoDid { 419 t.Fatalf("unexpected collaborator: %+v", c) 420 } 421 422 ok, err := e.IsRepoCollaborator(subjectDid.String(), rbac.ThisServer, repoDid.String()) 423 if err != nil || !ok { 424 t.Fatalf("Casbin policy for collaborator missing or err: %v", err) 425 } 426 427 err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ 428 Live: true, 429 Did: ownerDid, 430 Rkey: "collab-rkey-3", 431 Collection: syntax.NSID(tangled.RepoCollaboratorNSID), 432 Action: tapc.RecordDeleteAction, 433 }) 434 if err != nil { 435 t.Fatalf("delete collaborator process returned error: %v", err) 436 } 437 438 _, err = d.GetRepoCollaborator(ownerDid, "collab-rkey-3") 439 if err == nil { 440 t.Fatal("collaborator DB row remained after deletion") 441 } 442 443 ok, err = e.IsRepoCollaborator(subjectDid.String(), rbac.ThisServer, repoDid.String()) 444 if err != nil || ok { 445 t.Fatal("Casbin policy for collaborator remained after deletion") 446 } 447} 448 449func TestTeardownRepo_RBAC(t *testing.T) { 450 d, e := newTestSpindleDB(t) 451 452 cfg := &config.Config{} 453 cfg.Server.Hostname = "spindle.test" 454 455 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 456 if jcerr != nil { 457 t.Fatalf("NewJetstreamClient: %v", jcerr) 458 } 459 s := &Spindle{ 460 db: d, 461 e: e, 462 l: slog.Default(), 463 cfg: cfg, 464 jc: jc, 465 rootCtx: context.Background(), 466 } 467 468 tap := &Tap{ 469 spindle: s, 470 logger: slog.Default(), 471 } 472 473 ownerDid := syntax.DID("did:plc:repoowner") 474 repoDid := syntax.DID("did:plc:testrepo123") 475 collabDid := syntax.DID("did:plc:collab") 476 477 err := d.AddRepo(db.Repo{ 478 Knot: "knot.test", 479 Owner: ownerDid, 480 Rkey: "test-repo-rkey", 481 RepoDid: repoDid, 482 CreatedAt: time.Now().Format(time.RFC3339), 483 }) 484 if err != nil { 485 t.Fatalf("AddRepo DB: %v", err) 486 } 487 488 err = e.AddRepo(ownerDid.String(), rbac.ThisServer, repoDid.String()) 489 if err != nil { 490 t.Fatalf("AddRepo policy: %v", err) 491 } 492 493 err = d.AddRepoCollaborator(db.RepoCollaborator{ 494 OwnerDid: ownerDid, 495 Rkey: "collab-rkey", 496 Subject: collabDid, 497 RepoDid: repoDid, 498 }) 499 if err != nil { 500 t.Fatalf("AddCollaborator DB: %v", err) 501 } 502 503 err = e.AddCollaborator(collabDid.String(), rbac.ThisServer, repoDid.String()) 504 if err != nil { 505 t.Fatalf("AddCollaborator policy: %v", err) 506 } 507 508 err = tap.processRepo(context.Background(), &tapc.RecordEventData{ 509 Live: true, 510 Did: ownerDid, 511 Rkey: "test-repo-rkey", 512 Collection: syntax.NSID(tangled.RepoNSID), 513 Action: tapc.RecordDeleteAction, 514 }) 515 if err != nil { 516 t.Fatalf("processRepo delete returned error: %v", err) 517 } 518 519 _, err = d.GetRepoByOwnerRkey(ownerDid, "test-repo-rkey") 520 if err == nil { 521 t.Fatal("repo remained in DB after delete") 522 } 523 524 collabs, err := d.ListCollaboratorsByRepoDid(repoDid) 525 if err != nil { 526 t.Fatalf("ListCollaboratorsByRepoDid: %v", err) 527 } 528 if len(collabs) > 0 { 529 t.Fatal("collaborators remained in DB after delete") 530 } 531 532 ok, err := e.IsRepoOwner(ownerDid.String(), rbac.ThisServer, repoDid.String()) 533 if err != nil || ok { 534 t.Fatal("repo owner policy remained in Casbin after delete") 535 } 536 537 ok, err = e.IsRepoCollaborator(collabDid.String(), rbac.ThisServer, repoDid.String()) 538 if err != nil || ok { 539 t.Fatal("collaborator policy remained in Casbin after delete") 540 } 541} 542 543func TestProcessRepo_ForgeDeleteRejection(t *testing.T) { 544 d, e := newTestSpindleDB(t) 545 546 cfg := &config.Config{} 547 cfg.Server.Hostname = "spindle.test" 548 549 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 550 if jcerr != nil { 551 t.Fatalf("NewJetstreamClient: %v", jcerr) 552 } 553 554 s := &Spindle{ 555 db: d, 556 e: e, 557 l: slog.Default(), 558 cfg: cfg, 559 jc: jc, 560 rootCtx: context.Background(), 561 } 562 563 tap := &Tap{ 564 spindle: s, 565 logger: slog.Default(), 566 } 567 568 aliceDid := syntax.DID("did:plc:alice") 569 bobDid := syntax.DID("did:plc:bob") 570 repoDid := syntax.DID("did:plc:sharedrepo") 571 572 err := d.AddRepo(db.Repo{ 573 Knot: "knot.test", 574 Owner: aliceDid, 575 Rkey: "test-repo-rkey", 576 RepoDid: repoDid, 577 CreatedAt: time.Now().Format(time.RFC3339), 578 }) 579 if err != nil { 580 t.Fatalf("AddRepo DB: %v", err) 581 } 582 583 err = e.AddRepo(aliceDid.String(), rbac.ThisServer, repoDid.String()) 584 if err != nil { 585 t.Fatalf("AddRepo policy: %v", err) 586 } 587 588 // bob tries to delete alice's repo, must reject forged delete 589 err = tap.processRepo(context.Background(), &tapc.RecordEventData{ 590 Live: true, 591 Did: bobDid, 592 Rkey: "test-repo-rkey", 593 Collection: syntax.NSID(tangled.RepoNSID), 594 Action: tapc.RecordDeleteAction, 595 }) 596 if err != nil { 597 t.Fatalf("processRepo returned error on delete: %v", err) 598 } 599 600 _, err = d.GetRepoByOwnerRkey(aliceDid, "test-repo-rkey") 601 if err != nil { 602 t.Fatalf("Alice's repo was deleted or error: %v", err) 603 } 604 605 ok, err := e.IsRepoOwner(aliceDid.String(), rbac.ThisServer, repoDid.String()) 606 if err != nil || !ok { 607 t.Fatal("Alice's owner policy was removed from Casbin by forged delete") 608 } 609} 610 611func TestProcessCollaborator_ForgeDeleteRejection(t *testing.T) { 612 d, e := newTestSpindleDB(t) 613 614 cfg := &config.Config{} 615 cfg.Server.Hostname = "spindle.test" 616 617 jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) 618 if jcerr != nil { 619 t.Fatalf("NewJetstreamClient: %v", jcerr) 620 } 621 622 s := &Spindle{ 623 db: d, 624 e: e, 625 l: slog.Default(), 626 cfg: cfg, 627 res: idresolver.DefaultResolver("https://plc.test"), 628 jc: jc, 629 rootCtx: context.Background(), 630 } 631 632 tap := &Tap{ 633 spindle: s, 634 logger: slog.Default(), 635 } 636 637 ownerDid := syntax.DID("did:plc:repoowner") 638 bobDid := syntax.DID("did:plc:bob") 639 collabDid := syntax.DID("did:plc:collab") 640 repoDid := syntax.DID("did:plc:testrepo123") 641 642 err := d.AddRepo(db.Repo{ 643 Knot: "knot.test", 644 Owner: ownerDid, 645 Rkey: "test-repo-rkey", 646 RepoDid: repoDid, 647 CreatedAt: time.Now().Format(time.RFC3339), 648 }) 649 if err != nil { 650 t.Fatalf("AddRepo: %v", err) 651 } 652 653 err = e.AddRepo(ownerDid.String(), rbac.ThisServer, repoDid.String()) 654 if err != nil { 655 t.Fatalf("AddRepo permissions: %v", err) 656 } 657 658 err = d.AddRepoCollaborator(db.RepoCollaborator{ 659 OwnerDid: ownerDid, 660 Rkey: "collab-rkey", 661 Subject: collabDid, 662 RepoDid: repoDid, 663 }) 664 if err != nil { 665 t.Fatalf("AddRepoCollaborator: %v", err) 666 } 667 668 err = e.AddCollaborator(collabDid.String(), rbac.ThisServer, repoDid.String()) 669 if err != nil { 670 t.Fatalf("AddCollaborator policy: %v", err) 671 } 672 673 // bob tries to delete alice's collaborator, must reject forged delete 674 err = tap.processCollaborator(context.Background(), &tapc.RecordEventData{ 675 Live: true, 676 Did: bobDid, 677 Rkey: "collab-rkey", 678 Collection: syntax.NSID(tangled.RepoCollaboratorNSID), 679 Action: tapc.RecordDeleteAction, 680 }) 681 if err != nil { 682 t.Fatalf("processCollaborator delete returned error: %v", err) 683 } 684 685 _, err = d.GetRepoCollaborator(ownerDid, "collab-rkey") 686 if err != nil { 687 t.Fatalf("collaborator was deleted from DB: %v", err) 688 } 689 690 ok, err := e.IsRepoCollaborator(collabDid.String(), rbac.ThisServer, repoDid.String()) 691 if err != nil || !ok { 692 t.Fatal("collaborator policy was removed from Casbin by forged delete") 693 } 694}