Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
Go
at sl/no-verify
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442package spindle
import ( "context" "encoding/json" "log/slog" "strings" "tangled.org/core/jetstream" "testing" "time"
"github.com/bluesky-social/indigo/atproto/identity" "github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/api/tangled" "tangled.org/core/eventconsumer" "tangled.org/core/idresolver" "tangled.org/core/rbac" "tangled.org/core/spindle/config" "tangled.org/core/spindle/db"
"tangled.org/core/tapc")
type mockDirectory struct { ident *identity.Identity}
func (m *mockDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { return m.ident, nil}
func (m *mockDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { return m.ident, nil}
func (m *mockDirectory) Lookup(ctx context.Context, id syntax.AtIdentifier) (*identity.Identity, error) { return m.ident, nil}
func (m *mockDirectory) Purge(ctx context.Context, id syntax.AtIdentifier) error { return nil}
func TestProcessRepo_MembershipCheck(t *testing.T) { d, e := newTestSpindleDB(t)
cfg := &config.Config{} cfg.Server.Hostname = "spindle.test"
ccfg := eventconsumer.NewConsumerConfig() ccfg.Logger = slog.Default() ks := eventconsumer.NewConsumer(*ccfg)
jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) if jcerr != nil { t.Fatalf("NewJetstreamClient: %v", jcerr) } s := &Spindle{ db: d, e: e, l: slog.Default(), cfg: cfg, ks: ks, jc: jc, rootCtx: context.Background(), }
tap := &Tap{ spindle: s, logger: slog.Default(), }
ownerDid := syntax.DID("did:plc:memberowner") nonMemberDid := syntax.DID("did:plc:nonmemberowner") repoDid := syntax.DID("did:plc:testrepo123")
err := e.AddSpindleMember(rbac.ThisServer, ownerDid.String()) if err != nil { t.Fatalf("AddSpindleMember: %v", err) }
recNonMember := tangled.Repo{ Knot: "knot.test", RepoDid: (*string)(&repoDid), Spindle: &cfg.Server.Hostname, CreatedAt: time.Now().Format(time.RFC3339), } recNonMemberJson, _ := json.Marshal(recNonMember)
err = tap.processRepo(context.Background(), &tapc.RecordEventData{ Live: true, Did: nonMemberDid, Rkey: "test-repo-rkey", Collection: syntax.NSID(tangled.RepoNSID), Action: tapc.RecordCreateAction, Record: recNonMemberJson, }) if err != nil { t.Fatalf("processRepo returned error for non-member: %v", err) }
_, err = d.GetRepoByDid(repoDid) if err == nil { t.Fatal("repo for non-member was registered in DB, expected rejection") }
recMember := tangled.Repo{ Knot: "knot.test", RepoDid: (*string)(&repoDid), Spindle: &cfg.Server.Hostname, CreatedAt: time.Now().Format(time.RFC3339), } recMemberJson, _ := json.Marshal(recMember)
err = tap.processRepo(context.Background(), &tapc.RecordEventData{ Live: true, Did: ownerDid, Rkey: "test-repo-rkey", Collection: syntax.NSID(tangled.RepoNSID), Action: tapc.RecordCreateAction, Record: recMemberJson, }) if err == nil { t.Fatal("expected git clone error for valid member, but got nil") }
if !strings.Contains(err.Error(), "setting up sparse-clone git repo") { t.Fatalf("expected sparse-clone error, got: %v", err) }}
func TestProcessPull_IsCollaboratorCheck(t *testing.T) { d, e := newTestSpindleDB(t)
cfg := &config.Config{} cfg.Server.Hostname = "spindle.test"
jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) if jcerr != nil { t.Fatalf("NewJetstreamClient: %v", jcerr) } s := &Spindle{ db: d, e: e, l: slog.Default(), cfg: cfg, res: idresolver.DefaultResolver("https://plc.test"), jc: jc, rootCtx: context.Background(), }
repoOwnerDid := syntax.DID("did:plc:repoowner") nonPusherDid := syntax.DID("did:plc:nonpusher") pusherDid := syntax.DID("did:plc:pusher") repoDid := syntax.DID("did:plc:testrepo123")
err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: repoOwnerDid, Rkey: "test-repo-rkey", RepoDid: repoDid, CreatedAt: time.Now().Format(time.RFC3339), }) if err != nil { t.Fatalf("AddRepo: %v", err) }
err = e.SetRepoOwner(repoOwnerDid, repoDid) if err != nil { t.Fatalf("AddRepo permissions: %v", err) } err = e.AddRepoCollaborator(pusherDid, repoDid) if err != nil { t.Fatalf("AddCollaborator: %v", err) }
pullRecord := tangled.RepoPull{ Target: &tangled.RepoPull_Target{ Branch: "main", Repo: repoDid.String(), }, Source: &tangled.RepoPull_Source{ Branch: "feature", Repo: nil, // branch-based PR (source repo is nil) }, } pullRecordJson, _ := json.Marshal(pullRecord)
err = s.processPull(context.Background(), &tapc.RecordEventData{ Live: true, Did: nonPusherDid, Rkey: "pull-rkey-1", Collection: syntax.NSID(tangled.RepoPullNSID), Action: tapc.RecordCreateAction, Record: pullRecordJson, }) if err != nil { t.Fatalf("processPull returned error for non-pusher: %v", err) }
// fetch fails because plc/pds are not real err = s.processPull(context.Background(), &tapc.RecordEventData{ Live: true, Did: pusherDid, Rkey: "pull-rkey-2", Collection: syntax.NSID(tangled.RepoPullNSID), Action: tapc.RecordCreateAction, Record: pullRecordJson, }) if err == nil { t.Fatal("expected error from fetchLatestSubmission for valid pusher, but got nil") }
if !strings.Contains(err.Error(), "checking push access") && !strings.Contains(err.Error(), "resolve PR owner") && !strings.Contains(err.Error(), "invalid memory address") { t.Fatalf("expected failed identity resolution or connection error, got: %v", err) }}
func TestProcessRepo_HijackRepoDidCheck(t *testing.T) { d, e := newTestSpindleDB(t)
cfg := &config.Config{} cfg.Server.Hostname = "spindle.test"
ccfg := eventconsumer.NewConsumerConfig() ccfg.Logger = slog.Default() ks := eventconsumer.NewConsumer(*ccfg)
jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) if jcerr != nil { t.Fatalf("NewJetstreamClient: %v", jcerr) } s := &Spindle{ db: d, e: e, l: slog.Default(), cfg: cfg, ks: ks, jc: jc, rootCtx: context.Background(), }
tap := &Tap{ spindle: s, logger: slog.Default(), }
aliceDid := syntax.DID("did:plc:alice") bobDid := syntax.DID("did:plc:bob") repoDid := syntax.DID("did:plc:sharedrepo")
err := e.AddSpindleMember(rbac.ThisServer, aliceDid.String()) if err != nil { t.Fatalf("AddSpindleMember alice: %v", err) } err = e.AddSpindleMember(rbac.ThisServer, bobDid.String()) if err != nil { t.Fatalf("AddSpindleMember bob: %v", err) }
err = d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: aliceDid, Rkey: "alice-repo", RepoDid: syntax.DID(repoDid), CreatedAt: time.Now().Format(time.RFC3339), }) if err != nil { t.Fatalf("d.AddRepo: %v", err) }
// bob tries to register alice's repo did, must reject the hijack recBob := tangled.Repo{ Knot: "knot.test", RepoDid: (*string)(&repoDid), Spindle: &cfg.Server.Hostname, CreatedAt: time.Now().Format(time.RFC3339), } recBobJson, _ := json.Marshal(recBob)
err = tap.processRepo(context.Background(), &tapc.RecordEventData{ Live: true, Did: bobDid, Rkey: "bob-repo", Collection: syntax.NSID(tangled.RepoNSID), Action: tapc.RecordCreateAction, Record: recBobJson, }) if err != nil { t.Fatalf("processRepo returned error on duplicate repoDid hijack attempt: %v", err) }
_, err = d.GetRepoByDid(repoDid) if err == nil { t.Fatal("bob successfully hijacked alice's repoDid in DB, expected rejection") }}
func TestTeardownRepo_RBAC(t *testing.T) { d, e := newTestSpindleDB(t)
cfg := &config.Config{} cfg.Server.Hostname = "spindle.test"
jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) if jcerr != nil { t.Fatalf("NewJetstreamClient: %v", jcerr) } s := &Spindle{ db: d, e: e, l: slog.Default(), cfg: cfg, jc: jc, rootCtx: context.Background(), }
tap := &Tap{ spindle: s, logger: slog.Default(), }
ownerDid := syntax.DID("did:plc:repoowner") repoDid := syntax.DID("did:plc:testrepo123") collabDid := syntax.DID("did:plc:collab")
err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: ownerDid, Rkey: "test-repo-rkey", RepoDid: repoDid, CreatedAt: time.Now().Format(time.RFC3339), }) if err != nil { t.Fatalf("AddRepo DB: %v", err) }
err = e.SetRepoOwner(ownerDid, repoDid) if err != nil { t.Fatalf("AddRepo policy: %v", err) }
err = e.AddRepoCollaborator(collabDid, repoDid) if err != nil { t.Fatalf("AddCollaborator policy: %v", err) }
err = tap.processRepo(context.Background(), &tapc.RecordEventData{ Live: true, Did: ownerDid, Rkey: "test-repo-rkey", Collection: syntax.NSID(tangled.RepoNSID), Action: tapc.RecordDeleteAction, }) if err != nil { t.Fatalf("processRepo delete returned error: %v", err) }
_, err = d.GetRepoByDid(repoDid) if err == nil { t.Fatal("repo remained in DB after delete") }
ok, err := e.IsRepoOwner(ownerDid, repoDid) if err != nil || ok { t.Fatal("repo owner policy remained in Casbin after delete") }
ok, err = e.IsRepoCollaborator(collabDid, repoDid) if err != nil || ok { t.Fatal("collaborator policy remained in Casbin after delete") }}
func TestProcessRepo_ForgeDeleteRejection(t *testing.T) { d, e := newTestSpindleDB(t)
cfg := &config.Config{} cfg.Server.Hostname = "spindle.test"
jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) if jcerr != nil { t.Fatalf("NewJetstreamClient: %v", jcerr) }
s := &Spindle{ db: d, e: e, l: slog.Default(), cfg: cfg, jc: jc, rootCtx: context.Background(), }
tap := &Tap{ spindle: s, logger: slog.Default(), }
aliceDid := syntax.DID("did:plc:alice") bobDid := syntax.DID("did:plc:bob") repoDid := syntax.DID("did:plc:sharedrepo")
err := d.UpsertRepo(db.Repo{ Knot: "knot.test", Owner: aliceDid, Rkey: "test-repo-rkey", RepoDid: repoDid, CreatedAt: time.Now().Format(time.RFC3339), }) if err != nil { t.Fatalf("AddRepo DB: %v", err) }
err = e.SetRepoOwner(aliceDid, repoDid) if err != nil { t.Fatalf("AddRepo policy: %v", err) }
// bob tries to delete alice's repo, must reject forged delete err = tap.processRepo(context.Background(), &tapc.RecordEventData{ Live: true, Did: bobDid, Rkey: "test-repo-rkey", Collection: syntax.NSID(tangled.RepoNSID), Action: tapc.RecordDeleteAction, }) if err != nil { t.Fatalf("processRepo returned error on delete: %v", err) }
_, err = d.GetRepoByDid(repoDid) if err != nil { t.Fatalf("Alice's repo was deleted or error: %v", err) }
ok, err := e.IsRepoOwner(aliceDid, repoDid) if err != nil || !ok { t.Fatal("Alice's owner policy was removed from Casbin by forged delete") }}