diff --git a/remoteapi/integration_test.go b/remoteapi/integration_test.go index 947c8b40a81762e0680b93f25324f71700a068cb..1d270d22f607138dcd685536c3915588de365d04 100644 --- a/remoteapi/integration_test.go +++ b/remoteapi/integration_test.go @@ -11,7 +11,12 @@ // (4) PRIVATE anonymous clone fails "not found"; // (5) keypair (Bearer EdDSA JWT) clone of a PRIVATE own db succeeds after the // key is registered (dolt creds flow, no --user); // (6) WhoAmI answers with the account identity for a keypair JWT; -// (7) an ACL RO grantee can clone but not push (private repo stays "found"). +// (7) an ACL RO grantee can clone but not push (private repo stays "found"); +// (8) push-to-create provisions a repo + empty store on the first push; +// (9) push-to-create is refused in another user's namespace; +// (10) a database provisioned empty ahead of time (the web form, or git.sr.ht's +// hook via /internal/repos) accepts a first push from an unrelated local +// history with NO --force. // // Run with: // @@ -29,6 +34,7 @@ "database/sql" "encoding/base64" "encoding/hex" "fmt" + "log/slog" "net" "os" "os/exec" @@ -154,9 +160,15 @@ srv, err := New(cfg) if err != nil { t.Fatalf("New: %v", err) } + // The serve loops outlive the test body (GracefulStop is deferred), so their + // errors go to slog rather than to t: a t.Errorf after the test returns + // panics the run. This used to be a package-level logrus `logger` that the + // slog migration removed, which left this file failing to COMPILE — and + // nothing noticed, because the integration tag keeps it out of every default + // build and vet run. go func() { if err := srv.Serve(); err != nil { - logger.Errorf("remotesapi serve: %v", err) + slog.Error("remotesapi serve", "err", err) } }() defer srv.GracefulStop() @@ -167,7 +179,7 @@ t.Fatalf("NewCredServer: %v", err) } go func() { if err := csrv.Serve(); err != nil { - logger.Errorf("credentials serve: %v", err) + slog.Error("credentials serve", "err", err) } }() defer csrv.GracefulStop() @@ -394,6 +406,52 @@ if _, err := store.GetRepoByOwnerAndName(ctx, "alice", "bobtried"); err == nil { t.Fatalf("foreign-namespace push must not create a repository row") } t.Logf("foreign-namespace push-to-create correctly denied:\n%s", out) + }) + + // (10) A database provisioned ahead of the first push — the web create form, + // or git.sr.ht's post-update hook through /internal/repos — is created with + // an EMPTY store, and that is what lets its owner push a database they + // already have without --force. The local side here is deliberately not a + // fresh clone: it is an independent `dolt init` with its own root commit, + // the exact shape of a beads tracker or any locally grown database. With an + // "Initialize data repository" commit on the remote this push is a + // non-fast-forward and dolt refuses it client-side, which is the regression + // this test exists to catch. + t.Run("provisioned_empty_accepts_unrelated_history", func(t *testing.T) { + emptyAbs := storage.RepoDiskPath(reposRoot, "alice", "provisioned") + if err := storage.InitEmptyStore(ctx, emptyAbs); err != nil { + t.Fatalf("provision empty store: %v", err) + } + if _, err := store.CreateRepo(ctx, &core.Repo{ + Name: "provisioned", OwnerID: 1, OwnerName: "alice", + Path: emptyAbs, Visibility: core.VisibilityPrivate, + }); err != nil { + t.Fatalf("create provisioned repo row: %v", err) + } + + provURL := fmt.Sprintf("http://%s/~alice/provisioned", addr) + env := append(append([]string{}, baseEnv...), "DOLT_REMOTE_PASSWORD="+pat) + + work := filepath.Join(t.TempDir(), "provisioned") + if err := os.MkdirAll(work, 0o755); err != nil { + t.Fatal(err) + } + runDolt(t, env, work, "init") + runDolt(t, env, work, "sql", "-q", "create table beads(id int primary key); insert into beads values (7);") + runDolt(t, env, work, "commit", "-Am", "local history that predates the remote") + runDolt(t, env, work, "remote", "add", "origin", provURL) + + // No --force: runDolt fails the test on a non-zero exit, so a rejected + // non-fast-forward is the failure. + runDolt(t, env, work, "push", "--user", "alice", "origin", "main") + + dir := t.TempDir() + runDolt(t, env, dir, "clone", "--user", "alice", provURL) + out := runDolt(t, env, filepath.Join(dir, "provisioned"), "sql", "-q", "select id from beads", "-r", "csv") + if !strings.Contains(out, "7") { + t.Fatalf("pushed row not present after clone; got:\n%s", out) + } + t.Log("pre-provisioned empty database accepted an unrelated history without --force") }) }