Files
ReleaseKit/preparation_test.go

281 lines
11 KiB
Go

package releasekit_test
import (
"context"
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
)
func TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches(t *testing.T) {
repository := newTaggedDeliveryConsumer(t)
tagCommit := publishPreparationTag(t, repository)
advancePreparationCheckout(t, repository, "later local commit")
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository,
Tag: "v1.2.3",
ExpectedCommit: tagCommit,
}); err != nil {
t.Fatal(err)
}
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != tagCommit {
t.Fatalf("prepared HEAD = %s, want %s", head, tagCommit)
}
if branch := buildGit(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch != "HEAD" {
t.Fatalf("prepared checkout branch = %q, want detached HEAD", branch)
}
}
func TestPrepareTaggedCheckoutAcceptsNestedFullTag(t *testing.T) {
repository := newTaggedDeliveryConsumer(t)
tag := "integrations/ckconfig/v1.2.3"
tagCommit := publishPreparationTagNamed(t, repository, tag)
advancePreparationCheckout(t, repository, "later local commit")
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository, Tag: tag, ExpectedCommit: tagCommit,
}); err != nil {
t.Fatal(err)
}
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != tagCommit {
t.Fatalf("prepared HEAD = %s, want %s", head, tagCommit)
}
}
func TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout(t *testing.T) {
repository := newTaggedDeliveryConsumer(t)
tagCommit := publishPreparationTag(t, repository)
currentCommit := advancePreparationCheckout(t, repository, "later local commit")
remote := buildGit(t, repository, "remote", "get-url", "origin")
buildGit(t, repository, "remote", "add", "candidate", remote)
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository,
Tag: "v1.2.3",
Remote: "candidate",
ExpectedCommit: currentCommit,
})
if err == nil {
t.Fatal("PrepareTaggedCheckout() succeeded for a tag that resolves to another commit")
}
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
t.Fatalf("wrong-tag rejection changed HEAD to %s, want %s (tag commit %s)", head, currentCommit, tagCommit)
}
if branch := buildGit(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch == "HEAD" {
t.Fatal("wrong-tag rejection detached the checkout")
}
}
func TestPrepareTaggedCheckoutReportsLocalCheckoutConflict(t *testing.T) {
repository := newTaggedDeliveryConsumer(t)
tagCommit := publishPreparationTag(t, repository)
advancePreparationCheckout(t, repository, "later committed content")
path := filepath.Join(repository, "cmd/app/main.go")
if err := os.WriteFile(path, []byte("uncommitted conflicting content"), 0o600); err != nil {
t.Fatal(err)
}
currentCommit := buildGit(t, repository, "rev-parse", "HEAD")
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository,
Tag: "v1.2.3",
ExpectedCommit: tagCommit,
})
if err == nil {
t.Fatal("PrepareTaggedCheckout() succeeded despite a conflicting local edit")
}
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
t.Fatalf("conflicting checkout changed HEAD to %s, want %s", head, currentCommit)
}
contents, err := os.ReadFile(path)
if err != nil || string(contents) != "uncommitted conflicting content" {
t.Fatalf("conflicting local file = %q, %v", contents, err)
}
}
func TestPrepareTaggedCheckoutReportsConflictingLocalTag(t *testing.T) {
repository := newTaggedDeliveryConsumer(t)
publishPreparationTag(t, repository)
currentCommit := advancePreparationCheckout(t, repository, "later local commit")
buildGit(t, repository, "tag", "v1.2.3")
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository,
Tag: "v1.2.3",
ExpectedCommit: buildGit(t, repository, "rev-parse", "HEAD~1"),
})
if err == nil {
t.Fatal("PrepareTaggedCheckout() replaced a conflicting local tag")
}
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
t.Fatalf("conflicting tag fetch changed HEAD to %s, want %s", head, currentCommit)
}
if tagCommit := buildGit(t, repository, "rev-parse", "refs/tags/v1.2.3^{commit}"); tagCommit != currentCommit {
t.Fatalf("conflicting local tag changed to %s, want %s", tagCommit, currentCommit)
}
}
func TestPrepareTaggedCheckoutValidatesInputsBeforeFetching(t *testing.T) {
repository := newTaggedDeliveryConsumer(t)
publishPreparationTag(t, repository)
for _, request := range []releasekit.TaggedCheckoutRequest{
{RepositoryDir: repository, Tag: "not-a-version", ExpectedCommit: buildGit(t, repository, "rev-parse", "HEAD")},
{RepositoryDir: repository, Tag: "v1.2.3"},
{RepositoryDir: repository, Tag: "v1.2.3", ExpectedCommit: "not-an-object-id"},
} {
err := releasekit.PrepareTaggedCheckout(context.Background(), request)
if err == nil {
t.Fatalf("PrepareTaggedCheckout(%#v) succeeded", request)
}
if _, err := buildGitOutput(repository, "show-ref", "--verify", "refs/tags/v1.2.3"); err == nil {
t.Fatal("input validation fetched the tag before rejecting the request")
}
}
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository, Tag: "invalid", ExpectedCommit: "bad",
}); !errors.Is(err, releasekit.ErrInvalidVersion) {
t.Fatalf("invalid-version error = %v, want ErrInvalidVersion", err)
}
}
func TestPrepareTaggedCheckoutVerifyOnlyIsReadOnly(t *testing.T) {
for _, test := range []struct {
name string
wrong bool
mutate func(t *testing.T, repository string)
}{
{name: "wrong expected commit", wrong: true},
{name: "dirty checkout", mutate: func(t *testing.T, repository string) {
if err := os.WriteFile(filepath.Join(repository, "dirty.txt"), []byte("dirty"), 0o600); err != nil {
t.Fatal(err)
}
}},
{name: "missing tag", mutate: func(t *testing.T, repository string) {
buildGit(t, repository, "tag", "-d", "v1.2.3")
}},
{name: "moved tag", mutate: func(t *testing.T, repository string) {
advancePreparationCheckout(t, repository, "moved tagged commit")
buildGit(t, repository, "tag", "--force", "v1.2.3")
}},
} {
t.Run(test.name, func(t *testing.T) {
repository := newTaggedDeliveryConsumer(t)
tagCommit := publishPreparationTag(t, repository)
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository, Tag: "v1.2.3", ExpectedCommit: tagCommit,
}); err != nil {
t.Fatal(err)
}
if test.mutate != nil {
test.mutate(t, repository)
}
commit := tagCommit
if test.wrong {
commit = strings.Repeat("0", len(tagCommit))
}
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository, Tag: "v1.2.3", Remote: "missing-remote",
ExpectedCommit: commit, VerifyOnly: true,
}); err == nil {
t.Fatal("VerifyOnly accepted an inconsistent prepared checkout")
}
})
}
repository := newTaggedDeliveryConsumer(t)
tagCommit := publishPreparationTag(t, repository)
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository, Tag: "v1.2.3", ExpectedCommit: tagCommit,
}); err != nil {
t.Fatal(err)
}
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
RepositoryDir: repository, Tag: "v1.2.3", Remote: "missing-remote",
ExpectedCommit: tagCommit, VerifyOnly: true,
}); err != nil {
t.Fatalf("VerifyOnly rejected a prepared checkout: %v", err)
}
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != tagCommit {
t.Fatalf("VerifyOnly changed HEAD to %s, want %s", head, tagCommit)
}
if branch := buildGit(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch != "HEAD" {
t.Fatalf("VerifyOnly changed checkout branch to %q", branch)
}
}
func TestPrepareBuildDependenciesRequiresRepositoryDirectory(t *testing.T) {
if err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{RepositoryDir: " "}); err == nil {
t.Fatal("PrepareBuildDependencies() accepted an empty repository directory")
}
}
func TestPrepareBuildDependenciesUsesSelectedModuleDirectory(t *testing.T) {
repository := t.TempDir()
moduleDir := filepath.Join(repository, "integrations", "ckconfig")
if err := os.MkdirAll(moduleDir, 0o700); err != nil {
t.Fatal(err)
}
toolDir := t.TempDir()
marker := filepath.Join(t.TempDir(), "go-directory")
tool := filepath.Join(toolDir, "go")
if err := os.WriteFile(tool, []byte("#!/bin/sh\nprintf '%s' \"$PWD\" > \"$PREPARE_DIR_MARKER\"\n"), 0o700); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("PREPARE_DIR_MARKER", marker)
if err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{
RepositoryDir: repository,
ModuleDir: "integrations/ckconfig",
}); err != nil {
t.Fatal(err)
}
got, err := os.ReadFile(marker)
if err != nil || string(got) != moduleDir {
t.Fatalf("go mod download directory = %q, error=%v, want %q", got, err, moduleDir)
}
}
func publishPreparationTag(t *testing.T, repository string) string {
t.Helper()
return publishPreparationTagNamed(t, repository, "v1.2.3")
}
func publishPreparationTagNamed(t *testing.T, repository, tag string) string {
t.Helper()
ref := "refs/tags/" + tag
if _, err := buildGitOutput(repository, "show-ref", "--verify", "--quiet", ref); err != nil {
buildGit(t, repository, "tag", tag)
}
remoteDir := t.TempDir()
remote := filepath.Join(remoteDir, "origin.git")
buildGit(t, remoteDir, "init", "--bare", remote)
buildGit(t, repository, "remote", "add", "origin", remote)
commit := buildGit(t, repository, "rev-parse", ref+"^{commit}")
buildGit(t, repository, "push", "origin", ref)
buildGit(t, repository, "tag", "-d", tag)
return commit
}
func advancePreparationCheckout(t *testing.T, repository, source string) string {
t.Helper()
path := filepath.Join(repository, "cmd/app/main.go")
if err := os.WriteFile(path, []byte("package main\n// "+source+"\nfunc main() {}\n"), 0o600); err != nil {
t.Fatal(err)
}
buildGit(t, repository, "add", "cmd/app/main.go")
buildGit(t, repository, "commit", "-m", source)
return buildGit(t, repository, "rev-parse", "HEAD")
}
func buildGitOutput(repository string, args ...string) ([]byte, error) {
command := exec.Command("git", args...)
command.Dir = repository
return command.Output()
}