package spindle
import (
"context"
"fmt"
"net/http"
"sync"
"testing"
"github.com/bluesky-social/indigo/atproto/syntax"
"tangled.org/core/api/org_tangled"
"tangled.org/core/jetstream"
)
func (e *ensureEnv) markCollaboratorEvent(t *testing.T, who syntax.DID, op string) {
t.Helper()
evt, ok := jetstreamToTapEvent(collaboratorCommit(testRepoDid, who, op))
if !ok {
t.Fatalf("no tap event for %s", op)
}
if err := e.s.processEvent(withMarkOnly(context.Background()), evt); err != nil {
t.Fatal(err)
}
}
func TestAPastTheCapRepoStillTakesAGrantAndARevokeOneRecordAtATime(t *testing.T) {
env := newEnsureEnv(t)
env.serve("repo", testBob)
env.ensure(t)
env.wantPush(t, testBob, true)
// 3300 is one page past what the knot client reads, which means a sync can't finish in one go
for i := range 3300 {
env.grant(syntax.DID(fmt.Sprintf("did:plc:bulk%d", i)))
}
env.revoke(testBob)
env.markCollaboratorEvent(t, testBob, jetstream.CommitOperationDelete)
env.grant(testCarol)
env.markCollaboratorEvent(t, testCarol, jetstream.CommitOperationCreate)
before := env.knot.count()
env.s.runRepoJob(context.Background(), testRepoDid)
env.wantNoPushFor(t, testBob)
env.wantPush(t, testCarol, true)
if got := env.knot.count() - before; got > 40 {
t.Fatalf("the job made %d knot requests, one failed sync is about 35 and each record is one more", got)
}
if got := dueRecords(t, env); len(got) != 0 {
t.Fatalf("collaborator marks left behind: %+v", got)
}
if _, marked, err := env.s.db.RepoRetryRow(testRepoDid); err != nil || marked {
t.Fatalf("a repo that can't be synced whole keeps retrying, exists %v, %v", marked, err)
}
}
func TestACollaboratorEventOnASyncableRepoIsCoveredByTheFullSync(t *testing.T) {
env := newEnsureEnv(t)
env.serve("repo", testBob)
env.ensure(t)
env.grant(testCarol)
env.markCollaboratorEvent(t, testCarol, jetstream.CommitOperationCreate)
before := env.knot.count()
env.s.runRepoJob(context.Background(), testRepoDid)
env.wantPush(t, testCarol, true)
if got := env.knot.count() - before; got > 3 {
t.Fatalf("the job made %d knot requests, a refresh is 3 and the collaborator record needs no read of its own", got)
}
if got := dueRecords(t, env); len(got) != 0 {
t.Fatalf("the full sync left collaborator marks: %+v", got)
}
}
func TestACollaboratorMarkAppliedAfterAWipeDoesNotGrantAccessToTheWipedRepo(t *testing.T) {
env := newEnsureEnv(t)
env.serve("repo")
env.ensure(t)
env.grant(testBob)
env.markCollaboratorEvent(t, testBob, jetstream.CommitOperationCreate)
// the wipe lands during the mark's knot lookup, fired by the first collaborator query
var wiped sync.Once
env.knot.beforeAnswer = func(r *http.Request) {
if r.URL.Query().Get("collection") != org_tangled.GraphCollaboratorNSID {
return
}
wiped.Do(func() {
if err := env.s.WipeRepo(context.Background(), testRepoDid, "test"); err != nil {
t.Errorf("WipeRepo: %v", err)
}
})
}
env.s.runRecordMarks(context.Background(), testRepoDid)
if rows, err := env.s.db.ListCollaboratorsByRepoDid(testRepoDid); err != nil || len(rows) != 0 {
t.Fatalf("collaborator rows after the wipe = %+v, %v, want none", rows, err)
}
env.wantNoPushFor(t, testBob)
}