diff --git a/go/buildstep/fetch_integration_test.go b/go/buildstep/fetch_integration_test.go new file mode 100644 index 0000000..5f782a1 --- /dev/null +++ b/go/buildstep/fetch_integration_test.go @@ -0,0 +1,59 @@ +//go:build integration + +package buildstep + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/forgekey" +) + +func gitIn(t *testing.T, dir string, args ...string) string { + t.Helper() + cmd := exec.Command("git", append([]string{"-C", dir, "-c", "user.name=t", "-c", "user.email=t@example.invalid"}, args...)...) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("git %v: %v\n%s", args, err, out) + } + return strings.TrimSpace(string(out)) +} + +// 1474/AC5: a local bare repository with a commit only under refs/pull/1/head (a file:// remote, allowed by the +// test-only option): the workspace's HEAD is the commit. The forge itself is proven live after the build +// modules' delivery (ADR 0466 ยง5). +func TestFetchCommitFromAPullRefOnly(t *testing.T) { + root := t.TempDir() + bare := filepath.Join(root, "r.git") + work := filepath.Join(root, "work") + for _, d := range []string{bare, work} { + if err := os.MkdirAll(d, 0o755); err != nil { + t.Fatal(err) + } + } + gitIn(t, bare, "init", "-q", "--bare") + gitIn(t, work, "init", "-q") + if err := os.WriteFile(filepath.Join(work, "f"), []byte("x"), 0o644); err != nil { + t.Fatal(err) + } + gitIn(t, work, "add", "f") + gitIn(t, work, "commit", "-q", "-m", "pull request head") + commit := gitIn(t, work, "rev-parse", "HEAD") + gitIn(t, work, "push", "-q", bare, "HEAD:refs/pull/1/head") + + ws := filepath.Join(root, "ws") + if err := os.MkdirAll(ws, 0o755); err != nil { + t.Fatal(err) + } + err := FetchCommit(context.Background(), forgekey.ExecRunner, ws, "file://"+bare, commit, "refs/pull/1/head", AllowFileRemote()) + if err != nil { + t.Fatal(err) + } + if head := gitIn(t, ws, "rev-parse", "HEAD"); head != commit { + t.Errorf("HEAD = %s, want %s", head, commit) + } +} diff --git a/go/buildstep/fetch_test.go b/go/buildstep/fetch_test.go new file mode 100644 index 0000000..b989ab6 --- /dev/null +++ b/go/buildstep/fetch_test.go @@ -0,0 +1,137 @@ +package buildstep + +import ( + "context" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/forgekey" +) + +const sha = "0123456789abcdef0123456789abcdef01234567" + +// fakeGit records every command and answers by a rule. +type fakeGit struct { + calls []string + envs [][]string + rule func(args string) forgekey.Ran +} + +func (f *fakeGit) run(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { + line := name + " " + strings.Join(args, " ") + f.calls = append(f.calls, line) + f.envs = append(f.envs, env) + if f.rule != nil { + if r := f.rule(line); r.Status != 0 || r.Stdout != "" || r.Err != "" { + return r + } + } + if len(args) > 4 && args[4] == "rev-parse" { + return forgekey.Ran{Stdout: sha + "\n"} + } + return forgekey.Ran{} +} + +const pre = "git -c credential.helper= -C /w " + +// 1474/AC1: the git commands are exactly the sequence, none with a credential or an https remote. +func TestFetchCommitRunsTheSequence(t *testing.T) { + f := &fakeGit{} + if err := FetchCommit(context.Background(), f.run, "/w", "ssh://mesh-forge/novox/mesh-sdk.git", sha, ""); err != nil { + t.Fatal(err) + } + want := []string{ + pre + "init -q", + pre + "remote add origin ssh://mesh-forge/novox/mesh-sdk.git", + pre + "fetch --no-tags --depth 1 origin " + sha, + pre + "checkout -q --detach " + sha, + pre + "rev-parse HEAD", + } + if strings.Join(f.calls, "\n") != strings.Join(want, "\n") { + t.Errorf("commands:\n%s\nwant:\n%s", strings.Join(f.calls, "\n"), strings.Join(want, "\n")) + } + for i, c := range f.calls { + if strings.Contains(c, "https://") || strings.Contains(c, "@") || !strings.Contains(c, "credential.helper= ") { + t.Errorf("command %q carries a credential or https", c) + } + if len(f.envs[i]) != 1 || f.envs[i][0] != "GIT_TERMINAL_PROMPT=0" { + t.Errorf("env %v", f.envs[i]) + } + } +} + +// 1474/AC2: the depth-1 fetch fails and a pull ref is given: the ref is fetched and the commit verified; with no +// ref, the fetch's failure is the answer. +func TestFetchCommitFallsBackToThePullRef(t *testing.T) { + f := &fakeGit{rule: func(l string) forgekey.Ran { + if strings.Contains(l, "--depth 1") { + return forgekey.Ran{Status: 128, Stderr: "fatal: couldn't find remote ref " + sha + "\n"} + } + return forgekey.Ran{} + }} + if err := FetchCommit(context.Background(), f.run, "/w", "ssh://mesh-forge/o/r.git", sha, "refs/pull/1/head"); err != nil { + t.Fatal(err) + } + want := []string{pre + "fetch --no-tags origin refs/pull/1/head", pre + "cat-file -e " + sha + "^{commit}", pre + "checkout -q --detach " + sha} + got := strings.Join(f.calls[3:6], "\n") + if got != strings.Join(want, "\n") { + t.Errorf("after the failed fetch:\n%s\nwant:\n%s", got, strings.Join(want, "\n")) + } + + g := &fakeGit{rule: f.rule} + err := FetchCommit(context.Background(), g.run, "/w", "ssh://mesh-forge/o/r.git", sha, "") + if err == nil || !strings.Contains(err.Error(), "couldn't find remote ref") || len(g.calls) != 3 { + t.Errorf("without a ref: %v after %d commands", err, len(g.calls)) + } + + h := &fakeGit{rule: func(l string) forgekey.Ran { + if strings.Contains(l, "--depth 1") || strings.Contains(l, "cat-file") { + return forgekey.Ran{Status: 1, Stderr: "no"} + } + return forgekey.Ran{} + }} + if err := FetchCommit(context.Background(), h.run, "/w", "ssh://mesh-forge/o/r.git", sha, "refs/pull/1/head"); err == nil { + t.Error("a commit that is not in the ref was accepted") + } +} + +// 1474/AC3: HEAD differs from the commit after the checkout: an error naming both. +func TestFetchCommitAnswersAHeadThatIsNotTheCommit(t *testing.T) { + other := strings.Repeat("a", 40) + f := &fakeGit{rule: func(l string) forgekey.Ran { + if strings.HasSuffix(l, "rev-parse HEAD") { + return forgekey.Ran{Stdout: other + "\n"} + } + return forgekey.Ran{} + }} + err := FetchCommit(context.Background(), f.run, "/w", "ssh://mesh-forge/o/r.git", sha, "") + if err == nil || !strings.Contains(err.Error(), other) || !strings.Contains(err.Error(), sha) { + t.Errorf("error = %v, want both shas", err) + } +} + +// 1474/AC4: an https remote, user information, a file remote without the option, or a commit that is not 40 +// hex digits is refused before anything runs. +func TestFetchCommitRefusesBeforeRunningAnything(t *testing.T) { + for _, c := range []struct{ remote, commit, ref string }{ + {"https://forge.example/o/r.git", sha, ""}, + {"http://forge.example/o/r.git", sha, ""}, + {"ssh://git@mesh-forge/o/r.git", sha, ""}, + {"ssh://user:pw@mesh-forge/o/r.git", sha, ""}, + {"file:///tmp/r.git", sha, ""}, + {"/tmp/r.git", sha, ""}, + {"ssh://mesh-forge/o/r.git", "abc123", ""}, + {"ssh://mesh-forge/o/r.git", strings.ToUpper(sha), ""}, + {"ssh://mesh-forge/o/r.git", "main", ""}, + {"ssh://mesh-forge/o/r.git", sha, "--upload-pack=x"}, + } { + f := &fakeGit{} + if err := FetchCommit(context.Background(), f.run, "/w", c.remote, c.commit, c.ref); err == nil || len(f.calls) != 0 { + t.Errorf("%+v: err %v, %d commands", c, err, len(f.calls)) + } + } + f := &fakeGit{} + if err := FetchCommit(context.Background(), f.run, "/w", "file:///tmp/r.git", sha, "", AllowFileRemote()); err != nil { + t.Errorf("a file remote with the option: %v", err) + } +}