cli/pkg/cmd/release/edit/edit_test.go
Mislav Marohnić 36ffbe18de
Improve looking up draft releases by tag name
This changes the FetchRelease implementation to look up draft releases directly using by its pending tag name, as opposed to resorting to the Releases list API which is backed by Elastic Search and thus suffers replication lag after the creation of a draft release.

Bonus: all release lookup functions now accept a context for cancellation.
2022-12-14 21:24:08 +01:00

464 lines
11 KiB
Go

package edit
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"testing"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/release/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_NewCmdEdit(t *testing.T) {
tempDir := t.TempDir()
tf, err := os.CreateTemp(tempDir, "release-create")
require.NoError(t, err)
fmt.Fprint(tf, "MY NOTES")
tf.Close()
tests := []struct {
name string
args string
isTTY bool
stdin string
want EditOptions
wantErr string
}{
{
name: "no arguments notty",
args: "",
isTTY: false,
wantErr: "accepts 1 arg(s), received 0",
},
{
name: "provide title and notes",
args: "v1.2.3 --title 'Some Title' --notes 'Some Notes'",
isTTY: false,
want: EditOptions{
TagName: "",
Name: stringPtr("Some Title"),
Body: stringPtr("Some Notes"),
},
},
{
name: "provide discussion category",
args: "v1.2.3 --discussion-category some-category",
isTTY: false,
want: EditOptions{
TagName: "",
DiscussionCategory: stringPtr("some-category"),
},
},
{
name: "provide tag and target commitish",
args: "v1.2.3 --tag v9.8.7 --target 97ea5e77b4d61d5d80ed08f7512847dee3ec9af5",
isTTY: false,
want: EditOptions{
TagName: "v9.8.7",
Target: "97ea5e77b4d61d5d80ed08f7512847dee3ec9af5",
},
},
{
name: "provide prerelease",
args: "v1.2.3 --prerelease",
isTTY: false,
want: EditOptions{
TagName: "",
Prerelease: boolPtr(true),
},
},
{
name: "provide prerelease=false",
args: "v1.2.3 --prerelease=false",
isTTY: false,
want: EditOptions{
TagName: "",
Prerelease: boolPtr(false),
},
},
{
name: "provide draft",
args: "v1.2.3 --draft",
isTTY: false,
want: EditOptions{
TagName: "",
Draft: boolPtr(true),
},
},
{
name: "provide draft=false",
args: "v1.2.3 --draft=false",
isTTY: false,
want: EditOptions{
TagName: "",
Draft: boolPtr(false),
},
},
{
name: "latest",
args: "v1.2.3 --latest",
isTTY: false,
want: EditOptions{
TagName: "",
IsLatest: boolPtr(true),
},
},
{
name: "not latest",
args: "v1.2.3 --latest=false",
isTTY: false,
want: EditOptions{
TagName: "",
IsLatest: boolPtr(false),
},
},
{
name: "provide notes from file",
args: fmt.Sprintf(`v1.2.3 -F '%s'`, tf.Name()),
isTTY: false,
want: EditOptions{
TagName: "",
Body: stringPtr("MY NOTES"),
},
},
{
name: "provide notes from stdin",
args: "v1.2.3 -F -",
isTTY: false,
stdin: "MY NOTES",
want: EditOptions{
TagName: "",
Body: stringPtr("MY NOTES"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, stdin, _, _ := iostreams.Test()
if tt.stdin == "" {
ios.SetStdinTTY(tt.isTTY)
} else {
ios.SetStdinTTY(false)
fmt.Fprint(stdin, tt.stdin)
}
ios.SetStdoutTTY(tt.isTTY)
ios.SetStderrTTY(tt.isTTY)
f := &cmdutil.Factory{
IOStreams: ios,
}
var opts *EditOptions
cmd := NewCmdEdit(f, func(o *EditOptions) error {
opts = o
return nil
})
cmd.PersistentFlags().StringP("repo", "R", "", "")
argv, err := shlex.Split(tt.args)
require.NoError(t, err)
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
_, err = cmd.ExecuteC()
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
return
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.want.TagName, opts.TagName)
assert.Equal(t, tt.want.Target, opts.Target)
assert.Equal(t, tt.want.Name, opts.Name)
assert.Equal(t, tt.want.Body, opts.Body)
assert.Equal(t, tt.want.DiscussionCategory, opts.DiscussionCategory)
assert.Equal(t, tt.want.Draft, opts.Draft)
assert.Equal(t, tt.want.Prerelease, opts.Prerelease)
assert.Equal(t, tt.want.IsLatest, opts.IsLatest)
})
}
}
func Test_editRun(t *testing.T) {
tests := []struct {
name string
isTTY bool
opts EditOptions
httpStubs func(t *testing.T, reg *httpmock.Registry)
wantErr string
wantStdout string
wantStderr string
}{
{
name: "edit the tag name",
isTTY: true,
opts: EditOptions{
TagName: "v1.2.4",
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.4",
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit the target",
isTTY: true,
opts: EditOptions{
Target: "c0ff33",
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"target_commitish": "c0ff33",
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit the release name",
isTTY: true,
opts: EditOptions{
Name: stringPtr("Hot Release #1"),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"name": "Hot Release #1",
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit the discussion category",
isTTY: true,
opts: EditOptions{
DiscussionCategory: stringPtr("some-category"),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"discussion_category_name": "some-category",
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit the latest marker",
isTTY: false,
opts: EditOptions{
IsLatest: boolPtr(true),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"make_latest": "true",
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit the release name (empty)",
isTTY: true,
opts: EditOptions{
Name: stringPtr(""),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"name": "",
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit the release notes",
isTTY: true,
opts: EditOptions{
Body: stringPtr("Release Notes:\n- Fix Bug #1\n- Fix Bug #2"),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"body": "Release Notes:\n- Fix Bug #1\n- Fix Bug #2",
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit the release notes (empty)",
isTTY: true,
opts: EditOptions{
Body: stringPtr(""),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"body": "",
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit draft (true)",
isTTY: true,
opts: EditOptions{
Draft: boolPtr(true),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": true,
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit draft (false)",
isTTY: true,
opts: EditOptions{
Draft: boolPtr(false),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"draft": false,
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit prerelease (true)",
isTTY: true,
opts: EditOptions{
Prerelease: boolPtr(true),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"prerelease": true,
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
{
name: "edit prerelease (false)",
isTTY: true,
opts: EditOptions{
Prerelease: boolPtr(false),
},
httpStubs: func(t *testing.T, reg *httpmock.Registry) {
mockSuccessfulEditResponse(reg, func(params map[string]interface{}) {
assert.Equal(t, map[string]interface{}{
"tag_name": "v1.2.3",
"prerelease": false,
}, params)
})
},
wantStdout: "https://github.com/OWNER/REPO/releases/tag/v1.2.3\n",
wantStderr: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(tt.isTTY)
ios.SetStdinTTY(tt.isTTY)
ios.SetStderrTTY(tt.isTTY)
fakeHTTP := &httpmock.Registry{}
// defer reg.Verify(t) // FIXME: intermittetly fails due to StubFetchRelease internals
shared.StubFetchRelease(t, fakeHTTP, "OWNER", "REPO", "v1.2.3", `{
"id": 12345,
"tag_name": "v1.2.3"
}`)
if tt.httpStubs != nil {
tt.httpStubs(t, fakeHTTP)
}
tt.opts.IO = ios
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: fakeHTTP}, nil
}
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
return ghrepo.FromFullName("OWNER/REPO")
}
err := editRun("v1.2.3", &tt.opts)
if tt.wantErr != "" {
require.EqualError(t, err, tt.wantErr)
return
} else {
require.NoError(t, err)
}
assert.Equal(t, tt.wantStdout, stdout.String())
assert.Equal(t, tt.wantStderr, stderr.String())
})
}
}
func mockSuccessfulEditResponse(reg *httpmock.Registry, cb func(params map[string]interface{})) {
matcher := httpmock.REST("PATCH", "repos/OWNER/REPO/releases/12345")
responder := httpmock.RESTPayload(201, `{
"html_url": "https://github.com/OWNER/REPO/releases/tag/v1.2.3"
}`, cb)
reg.Register(matcher, responder)
}
func boolPtr(b bool) *bool {
return &b
}
func stringPtr(s string) *string {
return &s
}