diff --git a/server/handle_server_create_account.go b/server/handle_server_create_account.go index 62c9d56..fdaf526 100644 --- a/server/handle_server_create_account.go +++ b/server/handle_server_create_account.go @@ -17,6 +17,7 @@ import ( "github.com/bluesky-social/indigo/util" "github.com/haileyok/cocoon/internal/helpers" "github.com/haileyok/cocoon/models" + "github.com/ipfs/go-cid" "github.com/labstack/echo/v4" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" @@ -219,40 +220,23 @@ func (s *Server) handleCreateAccount(e echo.Context) error { } } - if request.Did == nil || *request.Did == "" { - bs := s.getBlockstore(signupDid) - - clk := syntax.NewTIDClock(0) - r := &atp.Repo{ - DID: syntax.DID(signupDid), - Clock: clk, - MST: mst.NewEmptyTree(), - RecordStore: bs, - } - - root, rev, err := commitRepo(context.TODO(), bs, r, urepo.SigningKey) - if err != nil { - logger.Error("error committing", "error", err) - return helpers.ServerError(e, nil) - } + root, rev, err := s.initializeGenesisRepo(context.TODO(), urepo.Did, urepo.SigningKey) + if err != nil { + logger.Error("error initializing genesis repo", "error", err) + return helpers.ServerError(e, nil) + } - if err := s.UpdateRepo(context.TODO(), urepo.Did, root, rev); err != nil { - logger.Error("error updating repo after commit", "error", err) - return helpers.ServerError(e, nil) - } + s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ + RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ + Did: urepo.Did, + Handle: to.StringPtr(request.Handle), + Seq: time.Now().UnixMicro(), // TODO: no + Time: time.Now().Format(util.ISO8601), + }, + }) - s.evtman.AddEvent(context.TODO(), &events.XRPCStreamEvent{ - RepoIdentity: &atproto.SyncSubscribeRepos_Identity{ - Did: urepo.Did, - Handle: to.StringPtr(request.Handle), - Seq: time.Now().UnixMicro(), // TODO: no - Time: time.Now().Format(util.ISO8601), - }, - }) - - if err := s.emitRepoSync(context.TODO(), urepo.Did, rev, root); err != nil { - logger.Error("error emitting repo sync event", "error", err) - } + if err := s.emitRepoSync(context.TODO(), urepo.Did, rev, root); err != nil { + logger.Error("error emitting repo sync event", "error", err) } sess, err := s.createSession(ctx, &urepo) @@ -277,3 +261,28 @@ func (s *Server) handleCreateAccount(e echo.Context) error { Did: signupDid, }) } + +// initializeGenesisRepo commits an empty MST for did and records it as the +// repo's current head. +func (s *Server) initializeGenesisRepo(ctx context.Context, did string, signingKey []byte) (cid.Cid, string, error) { + bs := s.getBlockstore(did) + + clk := syntax.NewTIDClock(0) + r := &atp.Repo{ + DID: syntax.DID(did), + Clock: clk, + MST: mst.NewEmptyTree(), + RecordStore: bs, + } + + root, rev, err := commitRepo(ctx, bs, r, signingKey) + if err != nil { + return cid.Undef, "", fmt.Errorf("commit genesis repo: %w", err) + } + + if err := s.UpdateRepo(ctx, did, root, rev); err != nil { + return cid.Undef, "", fmt.Errorf("update repo after genesis commit: %w", err) + } + + return root, rev, nil +} diff --git a/server/handle_server_create_account_test.go b/server/handle_server_create_account_test.go new file mode 100644 index 0000000..939df00 --- /dev/null +++ b/server/handle_server_create_account_test.go @@ -0,0 +1,95 @@ +package server + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/bluesky-social/indigo/atproto/atcrypto" + "github.com/bluesky-social/indigo/events" + "github.com/haileyok/cocoon/identity" +) + +// TestCreateAccountInitializesRepoForExistingDID asserts an account created +// via the existing-DID flow gets a genesis repo commit and #identity/#sync +// events, same as the flow where cocoon mints the DID itself. +func TestCreateAccountInitializesRepoForExistingDID(t *testing.T) { + s := newTestServer(t) + + persister, err := NewDbPersister(s.db.Client(), time.Hour) + if err != nil { + t.Fatalf("new persister: %v", err) + } + s.evtman = events.NewEventManager(persister) + + k, err := atcrypto.GeneratePrivateKeyK256() + if err != nil { + t.Fatalf("generate key: %v", err) + } + pub, err := k.PublicKey() + if err != nil { + t.Fatalf("derive public key: %v", err) + } + + const did = "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" + const handle = "alice.pds.test" + + cache := identity.NewMemCache(10) + if err := cache.PutDoc(did, &identity.DidDoc{ + Id: did, + VerificationMethods: []identity.DidDocVerificationMethod{ + { + Id: did + "#atproto", + Type: "Multikey", + Controller: did, + PublicKeyMultibase: pub.Multibase(), + }, + }, + }); err != nil { + t.Fatalf("seed passport cache: %v", err) + } + s.passport = identity.NewPassport(nil, cache) + + tok := mintServiceAuthToken(t, k.Bytes(), did, s.config.Did, "com.atproto.server.createAccount", time.Now().Add(time.Minute)) + + body, err := json.Marshal(map[string]string{ + "handle": handle, + "email": "alice@test.invalid", + "password": "correct-horse-battery-staple", + "did": did, + }) + if err != nil { + t.Fatalf("marshal request: %v", err) + } + + c, rec := newRequestContext(http.MethodPost, "/xrpc/com.atproto.server.createAccount", string(body), map[string]string{ + "authorization": "Bearer " + tok, + }) + + if err := s.handleCreateAccount(c); err != nil { + t.Fatalf("handleCreateAccount: %v", err) + } + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String()) + } + + urepo, err := s.getRepoActorByDid(context.Background(), did) + if err != nil { + t.Fatalf("getRepoActorByDid: %v", err) + } + if urepo.Repo.Rev == "" { + t.Fatal("repo has no rev after account creation via the existing-DID flow") + } + if len(urepo.Repo.Root) == 0 { + t.Fatal("repo has no root after account creation via the existing-DID flow") + } + + types := eventTypesFor(t, s, did) + for _, want := range []string{"identity", "sync"} { + if !contains(types, want) { + t.Fatalf("missing %q event after account creation; got %v", want, types) + } + } +}