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.
464 lines
11 KiB
Go
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
|
|
}
|