Merge branch 'trunk' into update-attestation-verify-table-output

This commit is contained in:
Meredith Lancaster 2025-01-15 14:25:38 -07:00 committed by GitHub
commit 423a764f2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 421 additions and 91 deletions

4
go.mod
View file

@ -40,7 +40,7 @@ require (
github.com/opentracing/opentracing-go v1.2.0
github.com/rivo/tview v0.0.0-20221029100920-c4a7e501810d
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc
github.com/sigstore/protobuf-specs v0.3.2
github.com/sigstore/protobuf-specs v0.3.3
github.com/sigstore/sigstore-go v0.6.2
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
@ -51,7 +51,7 @@ require (
golang.org/x/term v0.27.0
golang.org/x/text v0.21.0
google.golang.org/grpc v1.64.1
google.golang.org/protobuf v1.34.2
google.golang.org/protobuf v1.36.2
gopkg.in/h2non/gock.v1 v1.1.2
gopkg.in/yaml.v3 v3.0.1
)

8
go.sum
View file

@ -393,8 +393,8 @@ github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJL
github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
github.com/sigstore/protobuf-specs v0.3.2 h1:nCVARCN+fHjlNCk3ThNXwrZRqIommIeNKWwQvORuRQo=
github.com/sigstore/protobuf-specs v0.3.2/go.mod h1:RZ0uOdJR4OB3tLQeAyWoJFbNCBFrPQdcokntde4zRBA=
github.com/sigstore/protobuf-specs v0.3.3 h1:RMZQgXTD/pF7KW6b5NaRLYxFYZ/wzx44PQFXN2PEo5g=
github.com/sigstore/protobuf-specs v0.3.3/go.mod h1:vIhZ6Uor1a38+wvRrKcqL2PtYNlgoIW9lhzYzkyy4EU=
github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8=
github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc=
github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk=
@ -548,8 +548,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:
google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View file

@ -6,8 +6,10 @@ import (
"net/http"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -18,8 +20,10 @@ type DeleteOptions struct {
IO *iostreams.IOStreams
Config func() (gh.Config, error)
HttpClient func() (*http.Client, error)
Prompter prompter.Prompter
Selector string
Selector string
Confirmed bool
}
func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
@ -27,33 +31,51 @@ func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co
IO: f.IOStreams,
Config: f.Config,
HttpClient: f.HttpClient,
Prompter: f.Prompter,
}
cmd := &cobra.Command{
Use: "delete {<id> | <url>}",
Short: "Delete a gist",
Args: cmdutil.ExactArgs(1, "cannot delete: gist argument required"),
Long: heredoc.Docf(`
Delete a GitHub gist.
To delete a gist interactively, use %[1]sgh gist delete%[1]s with no arguments.
To delete a gist non-interactively, supply the gist id or url.
`, "`"),
Example: heredoc.Doc(`
# delete a gist interactively
gh gist delete
# delete a gist non-interactively
gh gist delete 1234
`),
Args: cobra.MaximumNArgs(1),
RunE: func(c *cobra.Command, args []string) error {
opts.Selector = args[0]
if !opts.IO.CanPrompt() && !opts.Confirmed {
return cmdutil.FlagErrorf("--yes required when not running interactively")
}
if !opts.IO.CanPrompt() && len(args) == 0 {
return cmdutil.FlagErrorf("id or url argument required in non-interactive mode")
}
if len(args) == 1 {
opts.Selector = args[0]
}
if runF != nil {
return runF(&opts)
}
return deleteRun(&opts)
},
}
cmd.Flags().BoolVar(&opts.Confirmed, "yes", false, "confirm deletion without prompting")
return cmd
}
func deleteRun(opts *DeleteOptions) error {
gistID := opts.Selector
if strings.Contains(gistID, "/") {
id, err := shared.GistIDFromURL(gistID)
if err != nil {
return err
}
gistID = id
}
client, err := opts.HttpClient()
if err != nil {
return err
@ -66,14 +88,56 @@ func deleteRun(opts *DeleteOptions) error {
host, _ := cfg.Authentication().DefaultHost()
gistID := opts.Selector
if strings.Contains(gistID, "/") {
id, err := shared.GistIDFromURL(gistID)
if err != nil {
return err
}
gistID = id
}
cs := opts.IO.ColorScheme()
var gist *shared.Gist
if gistID == "" {
gist, err = shared.PromptGists(opts.Prompter, client, host, cs)
} else {
gist, err = shared.GetGist(client, host, gistID)
}
if err != nil {
return err
}
if gist.ID == "" {
fmt.Fprintln(opts.IO.Out, "No gists found.")
return nil
}
if !opts.Confirmed {
confirmed, err := opts.Prompter.Confirm(fmt.Sprintf("Delete %q gist?", gist.Filename()), false)
if err != nil {
return err
}
if !confirmed {
return cmdutil.CancelError
}
}
apiClient := api.NewClientFromHTTP(client)
if err := deleteGist(apiClient, host, gistID); err != nil {
if err := deleteGist(apiClient, host, gist.ID); err != nil {
if errors.Is(err, shared.NotFoundErr) {
return fmt.Errorf("unable to delete gist %s: either the gist is not found or it is not owned by you", gistID)
return fmt.Errorf("unable to delete gist %q: either the gist is not found or it is not owned by you", gist.Filename())
}
return err
}
if opts.IO.IsStdoutTTY() {
cs := opts.IO.ColorScheme()
fmt.Fprintf(opts.IO.Out,
"%s Gist %q deleted\n",
cs.SuccessIcon(),
gist.Filename())
}
return nil
}

View file

@ -2,36 +2,95 @@ package delete
import (
"bytes"
"fmt"
"net/http"
"testing"
"time"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/gist/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
ghAPI "github.com/cli/go-gh/v2/pkg/api"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
)
func TestNewCmdDelete(t *testing.T) {
tests := []struct {
name string
cli string
wants DeleteOptions
name string
cli string
tty bool
want DeleteOptions
wantErr bool
wantErrMsg string
}{
{
name: "valid selector",
cli: "123",
wants: DeleteOptions{
tty: true,
want: DeleteOptions{
Selector: "123",
},
},
{
name: "valid selector, no ID supplied",
cli: "",
tty: true,
want: DeleteOptions{
Selector: "",
},
},
{
name: "no ID supplied with --yes",
cli: "--yes",
tty: true,
want: DeleteOptions{
Selector: "",
},
},
{
name: "selector with --yes, no tty",
cli: "123 --yes",
tty: false,
want: DeleteOptions{
Selector: "123",
},
},
{
name: "ID arg without --yes, no tty",
cli: "123",
tty: false,
want: DeleteOptions{
Selector: "",
},
wantErr: true,
wantErrMsg: "--yes required when not running interactively",
},
{
name: "no ID supplied with --yes, no tty",
cli: "--yes",
tty: false,
want: DeleteOptions{
Selector: "",
},
wantErr: true,
wantErrMsg: "id or url argument required in non-interactive mode",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &cmdutil.Factory{}
io, _, _, _ := iostreams.Test()
f := &cmdutil.Factory{
IOStreams: io,
}
io.SetStdinTTY(tt.tty)
io.SetStdoutTTY(tt.tty)
argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)
@ -47,69 +106,210 @@ func TestNewCmdDelete(t *testing.T) {
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
assert.NoError(t, err)
if tt.wantErr {
assert.EqualError(t, err, tt.wantErrMsg)
return
}
assert.Equal(t, tt.wants.Selector, gotOpts.Selector)
assert.NoError(t, err)
assert.Equal(t, tt.want.Selector, gotOpts.Selector)
})
}
}
func Test_deleteRun(t *testing.T) {
tests := []struct {
name string
opts DeleteOptions
httpStubs func(*httpmock.Registry)
wantErr bool
wantStdout string
wantStderr string
name string
opts *DeleteOptions
cancel bool
httpStubs func(*httpmock.Registry)
mockPromptGists bool
noGists bool
wantErr bool
wantStdout string
wantStderr string
}{
{
name: "successfully delete",
opts: DeleteOptions{
opts: &DeleteOptions{
Selector: "1234",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "gists/1234"),
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
reg.Register(httpmock.REST("DELETE", "gists/1234"),
httpmock.StatusStringResponse(200, "{}"))
},
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
},
{
name: "successfully delete with prompt",
opts: &DeleteOptions{
Selector: "",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("DELETE", "gists/1234"),
httpmock.StatusStringResponse(200, "{}"))
},
wantErr: false,
wantStdout: "",
wantStderr: "",
mockPromptGists: true,
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
},
{
name: "not found",
opts: DeleteOptions{
name: "successfully delete with --yes",
opts: &DeleteOptions{
Selector: "1234",
Confirmed: true,
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "gists/1234"),
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
reg.Register(httpmock.REST("DELETE", "gists/1234"),
httpmock.StatusStringResponse(200, "{}"))
},
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
},
{
name: "successfully delete with prompt and --yes",
opts: &DeleteOptions{
Selector: "",
Confirmed: true,
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("DELETE", "gists/1234"),
httpmock.StatusStringResponse(200, "{}"))
},
mockPromptGists: true,
wantStdout: "✓ Gist \"cool.txt\" deleted\n",
},
{
name: "cancel delete with id",
opts: &DeleteOptions{
Selector: "1234",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "gists/1234"),
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
},
cancel: true,
wantErr: true,
},
{
name: "cancel delete with url",
opts: &DeleteOptions{
Selector: "https://gist.github.com/myrepo/1234",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "gists/1234"),
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
},
cancel: true,
wantErr: true,
},
{
name: "cancel delete with prompt",
opts: &DeleteOptions{
Selector: "",
},
httpStubs: func(reg *httpmock.Registry) {},
mockPromptGists: true,
cancel: true,
wantErr: true,
},
{
name: "not owned by you",
opts: &DeleteOptions{
Selector: "1234",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "gists/1234"),
httpmock.JSONResponse(shared.Gist{ID: "1234", Files: map[string]*shared.GistFile{"cool.txt": {Filename: "cool.txt"}}}))
reg.Register(httpmock.REST("DELETE", "gists/1234"),
httpmock.StatusStringResponse(404, "{}"))
},
wantErr: true,
wantStdout: "",
wantStderr: "",
wantStderr: "unable to delete gist \"cool.txt\": either the gist is not found or it is not owned by you",
},
{
name: "not found",
opts: &DeleteOptions{
Selector: "1234",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", "gists/1234"),
httpmock.StatusStringResponse(404, "{}"))
},
wantErr: true,
wantStderr: "not found",
},
{
name: "no gists",
opts: &DeleteOptions{
Selector: "",
},
httpStubs: func(reg *httpmock.Registry) {},
mockPromptGists: true,
noGists: true,
wantStdout: "No gists found.\n",
},
}
for _, tt := range tests {
reg := &httpmock.Registry{}
if tt.httpStubs != nil {
tt.httpStubs(reg)
pm := prompter.NewMockPrompter(t)
if !tt.opts.Confirmed {
pm.RegisterConfirm("Delete \"cool.txt\" gist?", func(_ string, _ bool) (bool, error) {
return !tt.cancel, nil
})
}
reg := &httpmock.Registry{}
tt.httpStubs(reg)
if tt.mockPromptGists {
if tt.noGists {
reg.Register(
httpmock.GraphQL(`query GistList\b`),
httpmock.StringResponse(
`{ "data": { "viewer": { "gists": { "nodes": [] }} } }`),
)
} else {
sixHours, _ := time.ParseDuration("6h")
sixHoursAgo := time.Now().Add(-sixHours)
reg.Register(
httpmock.GraphQL(`query GistList\b`),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234",
"files": [{ "name": "cool.txt" }],
"updatedAt": "%s",
"isPublic": true
}
] } } } }`,
sixHoursAgo.Format(time.RFC3339),
)),
)
pm.RegisterSelect("Select a gist", []string{"cool.txt about 6 hours ago"}, func(_, _ string, _ []string) (int, error) {
return 0, nil
})
}
}
tt.opts.Prompter = pm
tt.opts.HttpClient = func() (*http.Client, error) {
return &http.Client{Transport: reg}, nil
}
tt.opts.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}
ios, _, _, _ := iostreams.Test()
ios.SetStdoutTTY(false)
ios, _, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(true)
ios.SetStdinTTY(false)
tt.opts.IO = ios
t.Run(tt.name, func(t *testing.T) {
err := deleteRun(&tt.opts)
err := deleteRun(tt.opts)
reg.Verify(t)
if tt.wantErr {
assert.Error(t, err)
@ -119,6 +319,75 @@ func Test_deleteRun(t *testing.T) {
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantStdout, stdout.String())
assert.Equal(t, tt.wantStderr, stderr.String())
})
}
}
func Test_gistDelete(t *testing.T) {
tests := []struct {
name string
httpStubs func(*httpmock.Registry)
hostname string
gistID string
wantErr error
}{
{
name: "successful delete",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("DELETE", "gists/1234"),
httpmock.StatusStringResponse(204, "{}"),
)
},
hostname: "github.com",
gistID: "1234",
wantErr: nil,
},
{
name: "when an gist is not found, it returns a NotFoundError",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("DELETE", "gists/1234"),
httpmock.StatusStringResponse(404, "{}"),
)
},
hostname: "github.com",
gistID: "1234",
wantErr: shared.NotFoundErr,
},
{
name: "when there is a non-404 error deleting the gist, that error is returned",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.REST("DELETE", "gists/1234"),
httpmock.StatusJSONResponse(500, `{"message": "arbitrary error"}`),
)
},
hostname: "github.com",
gistID: "1234",
wantErr: api.HTTPError{
HTTPError: &ghAPI.HTTPError{
StatusCode: 500,
Message: "arbitrary error",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reg := &httpmock.Registry{}
tt.httpStubs(reg)
client := api.NewClientFromHTTP(&http.Client{Transport: reg})
err := deleteGist(client, tt.hostname, tt.gistID)
if tt.wantErr != nil {
assert.ErrorAs(t, err, &tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}

View file

@ -108,15 +108,16 @@ func editRun(opts *EditOptions) error {
if gistID == "" {
cs := opts.IO.ColorScheme()
if gistID == "" {
gistID, err = shared.PromptGists(opts.Prompter, client, host, cs)
gist, err := shared.PromptGists(opts.Prompter, client, host, cs)
if err != nil {
return err
}
if gistID == "" {
if gist.ID == "" {
fmt.Fprintln(opts.IO.Out, "No gists found.")
return nil
}
gistID = gist.ID
}
}

View file

@ -39,6 +39,19 @@ type Gist struct {
Owner *GistOwner `json:"owner,omitempty"`
}
func (g Gist) Filename() string {
filenames := make([]string, 0, len(g.Files))
for fn := range g.Files {
filenames = append(filenames, fn)
}
sort.Strings(filenames)
return filenames[0]
}
func (g Gist) TruncDescription() string {
return text.Truncate(100, text.RemoveExcessiveWhitespace(g.Description))
}
var NotFoundErr = errors.New("not found")
func GetGist(client *http.Client, hostname, gistID string) (*Gist, error) {
@ -202,47 +215,29 @@ func IsBinaryContents(contents []byte) bool {
return isBinary
}
func PromptGists(prompter prompter.Prompter, client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) {
func PromptGists(prompter prompter.Prompter, client *http.Client, host string, cs *iostreams.ColorScheme) (gist *Gist, err error) {
gists, err := ListGists(client, host, 10, nil, false, "all")
if err != nil {
return "", err
return &Gist{}, err
}
if len(gists) == 0 {
return "", nil
return &Gist{}, nil
}
var opts []string
var gistIDs = make([]string, len(gists))
var opts = make([]string, len(gists))
for i, gist := range gists {
gistIDs[i] = gist.ID
description := ""
gistName := ""
if gist.Description != "" {
description = gist.Description
}
filenames := make([]string, 0, len(gist.Files))
for fn := range gist.Files {
filenames = append(filenames, fn)
}
sort.Strings(filenames)
gistName = filenames[0]
gistTime := text.FuzzyAgo(time.Now(), gist.UpdatedAt)
// TODO: support dynamic maxWidth
description = text.Truncate(100, text.RemoveExcessiveWhitespace(description))
opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime))
opts = append(opts, opt)
opts[i] = fmt.Sprintf("%s %s %s", cs.Bold(gist.Filename()), gist.TruncDescription(), cs.Gray(gistTime))
}
result, err := prompter.Select("Select a gist", "", opts)
if err != nil {
return "", err
return &Gist{}, err
}
return gistIDs[result], nil
return &gists[result], nil
}

View file

@ -93,12 +93,15 @@ func TestIsBinaryContents(t *testing.T) {
}
func TestPromptGists(t *testing.T) {
sixHours, _ := time.ParseDuration("6h")
sixHoursAgo := time.Now().Add(-sixHours)
sixHoursAgoFormatted := sixHoursAgo.Format(time.RFC3339Nano)
tests := []struct {
name string
prompterStubs func(pm *prompter.MockPrompter)
response string
wantOut string
gist *Gist
wantOut Gist
wantErr bool
}{
{
@ -112,21 +115,21 @@ func TestPromptGists(t *testing.T) {
},
response: `{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "gistid1",
"name": "1234",
"files": [{ "name": "cool.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "gistid2",
"name": "5678",
"files": [{ "name": "gistfile0.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
}
] } } } }`,
wantOut: "gistid1",
wantOut: Gist{ID: "1234", Files: map[string]*GistFile{"cool.txt": {Filename: "cool.txt"}}, UpdatedAt: sixHoursAgo, Public: true},
},
{
name: "multiple files, select second gist",
@ -139,26 +142,26 @@ func TestPromptGists(t *testing.T) {
},
response: `{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "gistid1",
"name": "1234",
"files": [{ "name": "cool.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "gistid2",
"name": "5678",
"files": [{ "name": "gistfile0.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
}
] } } } }`,
wantOut: "gistid2",
wantOut: Gist{ID: "5678", Files: map[string]*GistFile{"gistfile0.txt": {Filename: "gistfile0.txt"}}, UpdatedAt: sixHoursAgo, Public: true},
},
{
name: "no files",
response: `{ "data": { "viewer": { "gists": { "nodes": [] } } } }`,
wantOut: "",
wantOut: Gist{},
},
}
@ -166,15 +169,12 @@ func TestPromptGists(t *testing.T) {
for _, tt := range tests {
reg := &httpmock.Registry{}
const query = `query GistList\b`
sixHours, _ := time.ParseDuration("6h")
sixHoursAgo := time.Now().Add(-sixHours)
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
tt.response,
sixHoursAgo.Format(time.RFC3339),
sixHoursAgoFormatted,
)),
)
client := &http.Client{Transport: reg}
@ -185,9 +185,9 @@ func TestPromptGists(t *testing.T) {
tt.prompterStubs(mockPrompter)
}
gistID, err := PromptGists(mockPrompter, client, "github.com", ios.ColorScheme())
gist, err := PromptGists(mockPrompter, client, "github.com", ios.ColorScheme())
assert.NoError(t, err)
assert.Equal(t, tt.wantOut, gistID)
assert.Equal(t, tt.wantOut.ID, gist.ID)
reg.Verify(t)
})
}

View file

@ -89,15 +89,16 @@ func viewRun(opts *ViewOptions) error {
cs := opts.IO.ColorScheme()
if gistID == "" {
gistID, err = shared.PromptGists(opts.Prompter, client, hostname, cs)
gist, err := shared.PromptGists(opts.Prompter, client, hostname, cs)
if err != nil {
return err
}
if gistID == "" {
if gist.ID == "" {
fmt.Fprintln(opts.IO.Out, "No gists found.")
return nil
}
gistID = gist.ID
}
if opts.Web {