Files
github__gh-stack/cmd/push_test.go
2026-03-24 17:54:56 -04:00

228 lines
5.4 KiB
Go

package cmd
import (
"fmt"
"io"
"testing"
"github.com/github/gh-stack/internal/config"
"github.com/github/gh-stack/internal/git"
"github.com/github/gh-stack/internal/github"
"github.com/github/gh-stack/internal/stack"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGeneratePRBody(t *testing.T) {
tests := []struct {
name string
commitBody string
wantContains []string
}{
{
name: "empty commit body",
commitBody: "",
wantContains: []string{
"GitHub Stacks CLI",
feedbackBaseURL,
"<sub>",
},
},
{
name: "with commit body",
commitBody: "This is a detailed description\nof the change.",
wantContains: []string{
"This is a detailed description\nof the change.",
"GitHub Stacks CLI",
"<sub>",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generatePRBody(tt.commitBody)
for _, want := range tt.wantContains {
assert.Contains(t, got, want)
}
})
}
}
// newPushMock creates a MockOps pre-configured for push tests.
func newPushMock(tmpDir string, currentBranch string) *git.MockOps {
return &git.MockOps{
GitDirFn: func() (string, error) { return tmpDir, nil },
CurrentBranchFn: func() (string, error) { return currentBranch, nil },
ResolveRemoteFn: func(string) (string, error) { return "origin", nil },
PushFn: func(string, []string, bool, bool) error { return nil },
}
}
func TestPush_SkipPRs(t *testing.T) {
s := stack.Stack{
Trunk: stack.BranchRef{Branch: "main"},
Branches: []stack.BranchRef{
{Branch: "b1"},
{Branch: "b2"},
},
}
tmpDir := t.TempDir()
writeStackFile(t, tmpDir, s)
var pushCalls []pushCall
mock := newPushMock(tmpDir, "b1")
mock.PushFn = func(remote string, branches []string, force, atomic bool) error {
pushCalls = append(pushCalls, pushCall{remote, branches, force, atomic})
return nil
}
restore := git.SetOps(mock)
defer restore()
cfg, _, errR := config.NewTestConfig()
cfg.GitHubClientOverride = &github.MockClient{}
cmd := PushCmd(cfg)
cmd.SetArgs([]string{"--skip-prs"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute()
cfg.Err.Close()
_, _ = io.ReadAll(errR)
assert.NoError(t, err)
require.Len(t, pushCalls, 1)
assert.Equal(t, "origin", pushCalls[0].remote)
assert.Equal(t, []string{"b1", "b2"}, pushCalls[0].branches)
assert.True(t, pushCalls[0].force)
assert.True(t, pushCalls[0].atomic)
}
func TestPush_SkipsMergedBranches(t *testing.T) {
s := stack.Stack{
Trunk: stack.BranchRef{Branch: "main"},
Branches: []stack.BranchRef{
{Branch: "b1", PullRequest: &stack.PullRequestRef{Number: 1, Merged: true}},
{Branch: "b2"},
{Branch: "b3", PullRequest: &stack.PullRequestRef{Number: 3, Merged: true}},
},
}
tmpDir := t.TempDir()
writeStackFile(t, tmpDir, s)
var pushCalls []pushCall
mock := newPushMock(tmpDir, "b2")
mock.PushFn = func(remote string, branches []string, force, atomic bool) error {
pushCalls = append(pushCalls, pushCall{remote, branches, force, atomic})
return nil
}
restore := git.SetOps(mock)
defer restore()
cfg, _, errR := config.NewTestConfig()
cfg.GitHubClientOverride = &github.MockClient{}
cmd := PushCmd(cfg)
cmd.SetArgs([]string{"--skip-prs"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute()
cfg.Err.Close()
_, _ = io.ReadAll(errR)
assert.NoError(t, err)
require.Len(t, pushCalls, 1)
assert.Equal(t, []string{"b2"}, pushCalls[0].branches)
}
func TestPush_PushFailure(t *testing.T) {
s := stack.Stack{
Trunk: stack.BranchRef{Branch: "main"},
Branches: []stack.BranchRef{
{Branch: "b1"},
},
}
tmpDir := t.TempDir()
writeStackFile(t, tmpDir, s)
mock := newPushMock(tmpDir, "b1")
mock.PushFn = func(string, []string, bool, bool) error {
return fmt.Errorf("remote rejected")
}
restore := git.SetOps(mock)
defer restore()
cfg, _, errR := config.NewTestConfig()
cfg.GitHubClientOverride = &github.MockClient{}
cmd := PushCmd(cfg)
cmd.SetArgs([]string{"--skip-prs"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute()
cfg.Err.Close()
errOut, _ := io.ReadAll(errR)
output := string(errOut)
assert.ErrorIs(t, err, ErrSilent)
assert.Contains(t, output, "failed to push")
}
func TestPush_DefaultPRTitleBody(t *testing.T) {
t.Run("single_commit", func(t *testing.T) {
restore := git.SetOps(&git.MockOps{
LogRangeFn: func(base, head string) ([]git.CommitInfo, error) {
return []git.CommitInfo{
{Subject: "Add login page", Body: "Implements the OAuth flow"},
}, nil
},
})
defer restore()
title, body := defaultPRTitleBody("main", "feat-login")
assert.Equal(t, "Add login page", title)
assert.Equal(t, "Implements the OAuth flow", body)
})
t.Run("multiple_commits", func(t *testing.T) {
restore := git.SetOps(&git.MockOps{
LogRangeFn: func(base, head string) ([]git.CommitInfo, error) {
return []git.CommitInfo{
{Subject: "First commit"},
{Subject: "Second commit"},
}, nil
},
})
defer restore()
title, body := defaultPRTitleBody("main", "my-feature")
assert.Equal(t, "my feature", title)
assert.Equal(t, "", body)
})
}
func TestPush_Humanize(t *testing.T) {
tests := []struct {
input string
want string
}{
{"my-branch", "my branch"},
{"my_branch", "my branch"},
{"nobranch", "nobranch"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.want, humanize(tt.input))
})
}
}