cli/pkg/cmd/gist/list/list_test.go
Andy Feller e067eacd81 Refactor ColorScheme initializer
This commit completely removes the iostreams.NewColorScheme() initializer function in favor of exporting the type fields for greater clarity in its use.

The result being code specifying only the fields that matter to test cases.
2025-04-04 11:57:37 -04:00

713 lines
18 KiB
Go

package list
import (
"bytes"
"fmt"
"net/http"
"regexp"
"testing"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
"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"
)
func TestNewCmdList(t *testing.T) {
tests := []struct {
name string
cli string
wants ListOptions
wantsErr bool
}{
{
name: "no arguments",
wants: ListOptions{
Limit: 10,
Visibility: "all",
},
},
{
name: "public",
cli: "--public",
wants: ListOptions{
Limit: 10,
Visibility: "public",
},
},
{
name: "secret",
cli: "--secret",
wants: ListOptions{
Limit: 10,
Visibility: "secret",
},
},
{
name: "secret with explicit false value",
cli: "--secret=false",
wants: ListOptions{
Limit: 10,
Visibility: "all",
},
},
{
name: "public and secret",
cli: "--secret --public",
wants: ListOptions{
Limit: 10,
Visibility: "secret",
},
},
{
name: "limit",
cli: "--limit 30",
wants: ListOptions{
Limit: 30,
Visibility: "all",
},
},
{
name: "invalid limit",
cli: "--limit 0",
wantsErr: true,
},
{
name: "filter and include-content",
cli: "--filter octo --include-content",
wants: ListOptions{
Limit: 10,
Filter: regexp.MustCompilePOSIX("octo"),
IncludeContent: true,
Visibility: "all",
},
},
{
name: "invalid filter",
cli: "--filter octo(",
wantsErr: true,
},
{
name: "include content without filter",
cli: "--include-content",
wantsErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &cmdutil.Factory{}
argv, err := shlex.Split(tt.cli)
assert.NoError(t, err)
var gotOpts *ListOptions
cmd := NewCmdList(f, func(opts *ListOptions) error {
gotOpts = opts
return nil
})
cmd.SetArgs(argv)
cmd.SetIn(&bytes.Buffer{})
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
_, err = cmd.ExecuteC()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wants.Visibility, gotOpts.Visibility)
assert.Equal(t, tt.wants.Limit, gotOpts.Limit)
})
}
}
func Test_listRun(t *testing.T) {
const query = `query GistList\b`
sixHours, _ := time.ParseDuration("6h")
sixHoursAgo := time.Now().Add(-sixHours)
absTime, _ := time.Parse(time.RFC3339, "2020-07-30T15:24:28Z")
tests := []struct {
name string
opts *ListOptions
wantErr bool
wantOut string
stubs func(*httpmock.Registry)
color bool
nontty bool
}{
{
name: "no gists",
opts: &ListOptions{},
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(`{ "data": { "viewer": {
"gists": { "nodes": [] }
} } }`))
},
wantErr: true,
},
{
name: "default behavior",
opts: &ListOptions{},
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234567890",
"files": [{ "name": "cool.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "4567890123",
"files": [{ "name": "gistfile0.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "2345678901",
"files": [
{ "name": "gistfile0.txt" },
{ "name": "gistfile1.txt" }
],
"description": "tea leaves thwart those who court catastrophe",
"updatedAt": "%[1]v",
"isPublic": false
},
{
"name": "3456789012",
"files": [
{ "name": "gistfile0.txt" },
{ "name": "gistfile1.txt" },
{ "name": "gistfile2.txt" },
{ "name": "gistfile3.txt" },
{ "name": "gistfile4.txt" },
{ "name": "gistfile5.txt" },
{ "name": "gistfile6.txt" },
{ "name": "gistfile7.txt" },
{ "name": "gistfile9.txt" },
{ "name": "gistfile10.txt" },
{ "name": "gistfile11.txt" }
],
"description": "short desc",
"updatedAt": "%[1]v",
"isPublic": false
}
] } } } }`,
sixHoursAgo.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Doc(`
ID DESCRIPTION FILES VISIBILITY UPDATED
1234567890 cool.txt 1 file public about 6 hours ago
4567890123 1 file public about 6 hours ago
2345678901 tea leaves thwart those ... 2 files secret about 6 hours ago
3456789012 short desc 11 files secret about 6 hours ago
`),
},
{
name: "with public filter",
opts: &ListOptions{Visibility: "public"},
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234567890",
"files": [{ "name": "cool.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "4567890123",
"files": [{ "name": "gistfile0.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
}
] } } } }`,
sixHoursAgo.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Doc(`
ID DESCRIPTION FILES VISIBILITY UPDATED
1234567890 cool.txt 1 file public about 6 hours ago
4567890123 1 file public about 6 hours ago
`),
},
{
name: "with secret filter",
opts: &ListOptions{Visibility: "secret"},
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "2345678901",
"files": [
{ "name": "gistfile0.txt" },
{ "name": "gistfile1.txt" }
],
"description": "tea leaves thwart those who court catastrophe",
"updatedAt": "%[1]v",
"isPublic": false
},
{
"name": "3456789012",
"files": [
{ "name": "gistfile0.txt" },
{ "name": "gistfile1.txt" },
{ "name": "gistfile2.txt" },
{ "name": "gistfile3.txt" },
{ "name": "gistfile4.txt" },
{ "name": "gistfile5.txt" },
{ "name": "gistfile6.txt" },
{ "name": "gistfile7.txt" },
{ "name": "gistfile9.txt" },
{ "name": "gistfile10.txt" },
{ "name": "gistfile11.txt" }
],
"description": "short desc",
"updatedAt": "%[1]v",
"isPublic": false
}
] } } } }`,
sixHoursAgo.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Doc(`
ID DESCRIPTION FILES VISIBILITY UPDATED
2345678901 tea leaves thwart those ... 2 files secret about 6 hours ago
3456789012 short desc 11 files secret about 6 hours ago
`),
},
{
name: "with limit",
opts: &ListOptions{Limit: 1},
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234567890",
"files": [{ "name": "cool.txt" }],
"description": "",
"updatedAt": "%v",
"isPublic": true
}
] } } } }`,
sixHoursAgo.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Doc(`
ID DESCRIPTION FILES VISIBILITY UPDATED
1234567890 cool.txt 1 file public about 6 hours ago
`),
},
{
name: "nontty output",
opts: &ListOptions{},
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234567890",
"files": [{ "name": "cool.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "4567890123",
"files": [{ "name": "gistfile0.txt" }],
"description": "",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "2345678901",
"files": [
{ "name": "gistfile0.txt" },
{ "name": "gistfile1.txt" }
],
"description": "tea leaves thwart those who court catastrophe",
"updatedAt": "%[1]v",
"isPublic": false
},
{
"name": "3456789012",
"files": [
{ "name": "gistfile0.txt" },
{ "name": "gistfile1.txt" },
{ "name": "gistfile2.txt" },
{ "name": "gistfile3.txt" },
{ "name": "gistfile4.txt" },
{ "name": "gistfile5.txt" },
{ "name": "gistfile6.txt" },
{ "name": "gistfile7.txt" },
{ "name": "gistfile9.txt" },
{ "name": "gistfile10.txt" },
{ "name": "gistfile11.txt" }
],
"description": "short desc",
"updatedAt": "%[1]v",
"isPublic": false
}
] } } } }`,
absTime.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Doc(`
1234567890 cool.txt 1 file public 2020-07-30T15:24:28Z
4567890123 1 file public 2020-07-30T15:24:28Z
2345678901 tea leaves thwart those who court catastrophe 2 files secret 2020-07-30T15:24:28Z
3456789012 short desc 11 files secret 2020-07-30T15:24:28Z
`),
nontty: true,
},
{
name: "filtered",
opts: &ListOptions{
Filter: regexp.MustCompile("octo"),
Visibility: "all",
},
nontty: true,
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234",
"files": [
{ "name": "main.txt", "text": "foo" }
],
"description": "octo match in the description",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "2345",
"files": [
{ "name": "main.txt", "text": "foo" },
{ "name": "octo.txt", "text": "bar" }
],
"description": "match in the file name",
"updatedAt": "%[1]v",
"isPublic": false
},
{
"name": "3456",
"files": [
{ "name": "main.txt", "text": "octo in the text" }
],
"description": "match in the file text",
"updatedAt": "%[1]v",
"isPublic": true
}
] } } } }`,
absTime.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Docf(`
1234%[1]socto match in the description%[1]s1 file%[1]spublic%[1]s2020-07-30T15:24:28Z
2345%[1]smatch in the file name%[1]s2 files%[1]ssecret%[1]s2020-07-30T15:24:28Z
`, "\t"),
},
{
name: "filtered (tty)",
opts: &ListOptions{
Filter: regexp.MustCompile("octo"),
Visibility: "all",
},
color: true,
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234",
"files": [
{ "name": "main.txt", "text": "foo" }
],
"description": "octo match in the description",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "2345",
"files": [
{ "name": "main.txt", "text": "foo" },
{ "name": "octo.txt", "text": "bar" }
],
"description": "match in the file name",
"updatedAt": "%[1]v",
"isPublic": false
},
{
"name": "3456",
"files": [
{ "name": "main.txt", "text": "octo in the text" }
],
"description": "match in the file text",
"updatedAt": "%[1]v",
"isPublic": true
}
] } } } }`,
sixHoursAgo.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Docf(`
%[1]s[0;4;39mID %[1]s[0m %[1]s[0;4;39mDESCRIPTION %[1]s[0m %[1]s[0;4;39mFILES %[1]s[0m %[1]s[0;4;39mVISIBILITY%[1]s[0m %[1]s[0;4;39mUPDATED %[1]s[0m
1234 %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m 1 file %[1]s[0;32mpublic %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[0m
2345 %[1]s[0;1;39mmatch in the file name %[1]s[0m %[1]s[0;30;43m2 files%[1]s[0m %[1]s[0;31msecret %[1]s[0m %[1]s[38;5;242mabout 6 hours ago%[1]s[0m
`, "\x1b"),
},
{
name: "filtered with content",
opts: &ListOptions{
Filter: regexp.MustCompile("octo"),
IncludeContent: true,
Visibility: "all",
},
nontty: true,
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234",
"files": [
{ "name": "main.txt", "text": "foo" }
],
"description": "octo match in the description",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "2345",
"files": [
{ "name": "main.txt", "text": "foo" },
{ "name": "octo.txt", "text": "bar" }
],
"description": "match in the file name",
"updatedAt": "%[1]v",
"isPublic": false
},
{
"name": "3456",
"files": [
{ "name": "main.txt", "text": "octo in the text" }
],
"description": "match in the file text",
"updatedAt": "%[1]v",
"isPublic": true
}
] } } } }`,
absTime.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Doc(`
1234 main.txt
octo match in the description
2345 octo.txt
match in the file name
3456 main.txt
match in the file text
octo in the text
`),
},
{
name: "filtered with content (tty)",
opts: &ListOptions{
Filter: regexp.MustCompile("octo"),
IncludeContent: true,
Visibility: "all",
},
color: true,
stubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(query),
httpmock.StringResponse(fmt.Sprintf(
`{ "data": { "viewer": { "gists": { "nodes": [
{
"name": "1234",
"files": [
{ "name": "main.txt", "text": "foo" }
],
"description": "octo match in the description",
"updatedAt": "%[1]v",
"isPublic": true
},
{
"name": "2345",
"files": [
{ "name": "main.txt", "text": "foo" },
{ "name": "octo.txt", "text": "bar" }
],
"description": "match in the file name",
"updatedAt": "%[1]v",
"isPublic": false
},
{
"name": "3456",
"files": [
{ "name": "main.txt", "text": "octo in the text" }
],
"description": "match in the file text",
"updatedAt": "%[1]v",
"isPublic": true
}
] } } } }`,
sixHoursAgo.Format(time.RFC3339),
)),
)
},
wantOut: heredoc.Docf(`
%[1]s[0;34m1234%[1]s[0m %[1]s[0;32mmain.txt%[1]s[0m
%[1]s[0;30;43mocto%[1]s[0m%[1]s[0;1;39m match in the description%[1]s[0m
%[1]s[0;34m2345%[1]s[0m %[1]s[0;30;43mocto%[1]s[0m%[1]s[0;32m.txt%[1]s[0m
%[1]s[0;1;39mmatch in the file name%[1]s[0m
%[1]s[0;34m3456%[1]s[0m %[1]s[0;32mmain.txt%[1]s[0m
%[1]s[0;1;39mmatch in the file text%[1]s[0m
%[1]s[0;30;43mocto%[1]s[0m in the text
`, "\x1b"),
},
}
for _, tt := range tests {
reg := &httpmock.Registry{}
tt.stubs(reg)
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, _, stdout, _ := iostreams.Test()
ios.SetColorEnabled(tt.color)
ios.SetStdoutTTY(!tt.nontty)
tt.opts.IO = ios
if tt.opts.Limit == 0 {
tt.opts.Limit = 10
}
if tt.opts.Visibility == "" {
tt.opts.Visibility = "all"
}
t.Run(tt.name, func(t *testing.T) {
err := listRun(tt.opts)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.wantOut, stdout.String())
reg.Verify(t)
})
}
}
func Test_highlightMatch(t *testing.T) {
regex := regexp.MustCompilePOSIX(`[Oo]cto`)
tests := []struct {
name string
input string
cs *iostreams.ColorScheme
want string
}{
{
name: "single match",
input: "Octo",
cs: &iostreams.ColorScheme{},
want: "Octo",
},
{
name: "single match (color)",
input: "Octo",
cs: &iostreams.ColorScheme{
Enabled: true,
},
want: "\x1b[0;30;43mOcto\x1b[0m",
},
{
name: "single match with extra",
input: "Hello, Octocat!",
cs: &iostreams.ColorScheme{},
want: "Hello, Octocat!",
},
{
name: "single match with extra (color)",
input: "Hello, Octocat!",
cs: &iostreams.ColorScheme{
Enabled: true,
},
want: "\x1b[0;34mHello, \x1b[0m\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat!\x1b[0m",
},
{
name: "multiple matches",
input: "Octocat/octo",
cs: &iostreams.ColorScheme{},
want: "Octocat/octo",
},
{
name: "multiple matches (color)",
input: "Octocat/octo",
cs: &iostreams.ColorScheme{
Enabled: true,
},
want: "\x1b[0;30;43mOcto\x1b[0m\x1b[0;34mcat/\x1b[0m\x1b[0;30;43mocto\x1b[0m",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
matched := false
got, err := highlightMatch(tt.input, regex, &matched, tt.cs.Blue, tt.cs.Highlight)
assert.NoError(t, err)
assert.True(t, matched)
assert.Equal(t, tt.want, got)
})
}
}