cli/pkg/cmd/factory/default_test.go
Andy Feller 3eca268a7f Introduce color_labels support, update commands
This commit implements the actual changes around configuration setting / environment variable logic for displaying labels using their RGB hex color code in terminals with truecolor support.

One of the subtler changes in this commit is renaming generic ColorScheme.HexToRGB logic to render truecolor to ColorScheme.Label as this feature was being used exclusively for labels.  This is due to confusion about introducing the new `color_labels` config on top of generic coloring logic.
2025-04-02 18:24:20 -04:00

586 lines
14 KiB
Go

package factory
import (
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"testing"
"github.com/cli/cli/v2/git"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/internal/gh"
ghmock "github.com/cli/cli/v2/internal/gh/mock"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_BaseRepo(t *testing.T) {
tests := []struct {
name string
remotes git.RemoteSet
override string
wantsErr bool
wantsName string
wantsOwner string
wantsHost string
}{
{
name: "matching remote",
remotes: git.RemoteSet{
git.NewRemote("origin", "https://nonsense.com/owner/repo.git"),
},
wantsName: "repo",
wantsOwner: "owner",
wantsHost: "nonsense.com",
},
{
name: "no matching remote",
remotes: git.RemoteSet{
git.NewRemote("origin", "https://test.com/owner/repo.git"),
},
wantsErr: true,
},
{
name: "override with matching remote",
remotes: git.RemoteSet{
git.NewRemote("origin", "https://test.com/owner/repo.git"),
},
override: "test.com",
wantsName: "repo",
wantsOwner: "owner",
wantsHost: "test.com",
},
{
name: "override with no matching remote",
remotes: git.RemoteSet{
git.NewRemote("origin", "https://nonsense.com/owner/repo.git"),
},
override: "test.com",
wantsErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := New("1")
rr := &remoteResolver{
readRemotes: func() (git.RemoteSet, error) {
return tt.remotes, nil
},
getConfig: func() (gh.Config, error) {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
hosts := []string{"nonsense.com"}
if tt.override != "" {
hosts = append([]string{tt.override}, hosts...)
}
authCfg.SetHosts(hosts)
authCfg.SetActiveToken("", "")
authCfg.SetDefaultHost("nonsense.com", "hosts")
if tt.override != "" {
authCfg.SetDefaultHost(tt.override, "GH_HOST")
}
return authCfg
}
return cfg, nil
},
}
f.Remotes = rr.Resolver()
f.BaseRepo = BaseRepoFunc(f)
repo, err := f.BaseRepo()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantsName, repo.RepoName())
assert.Equal(t, tt.wantsOwner, repo.RepoOwner())
assert.Equal(t, tt.wantsHost, repo.RepoHost())
})
}
}
func Test_SmartBaseRepo(t *testing.T) {
pu, _ := url.Parse("https://test.com/newowner/newrepo.git")
tests := []struct {
name string
remotes git.RemoteSet
override string
wantsErr bool
wantsName string
wantsOwner string
wantsHost string
tty bool
httpStubs func(*httpmock.Registry)
}{
{
name: "override with matching remote",
remotes: git.RemoteSet{
git.NewRemote("origin", "https://test.com/owner/repo.git"),
},
override: "test.com",
wantsName: "repo",
wantsOwner: "owner",
wantsHost: "test.com",
},
{
name: "override with matching remote and base resolution",
remotes: git.RemoteSet{
&git.Remote{Name: "origin",
Resolved: "base",
FetchURL: pu,
PushURL: pu},
},
override: "test.com",
wantsName: "newrepo",
wantsOwner: "newowner",
wantsHost: "test.com",
},
{
name: "override with matching remote and nonbase resolution",
remotes: git.RemoteSet{
&git.Remote{Name: "origin",
Resolved: "johnny/test",
FetchURL: pu,
PushURL: pu},
},
override: "test.com",
wantsName: "test",
wantsOwner: "johnny",
wantsHost: "test.com",
},
{
name: "override with no matching remote",
remotes: git.RemoteSet{
git.NewRemote("origin", "https://example.com/owner/repo.git"),
},
override: "test.com",
wantsErr: true,
},
{
name: "only one remote",
remotes: git.RemoteSet{
git.NewRemote("origin", "https://github.com/owner/repo.git"),
},
wantsName: "repo",
wantsOwner: "owner",
wantsHost: "github.com",
tty: true,
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("RepositoryNetwork"),
httpmock.StringResponse(`
{
"data": {
"viewer": {
"login": "someone"
},
"repo_000": {
"id": "MDEwOlJlcG9zaXRvcnkxMDM3MjM2Mjc=",
"name": "repo",
"owner": {
"login": "owner"
},
"viewerPermission": "READ",
"defaultBranchRef": {
"name": "master"
},
"isPrivate": false,
"parent": null
}
}
}
`))
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := New("1")
rr := &remoteResolver{
readRemotes: func() (git.RemoteSet, error) {
return tt.remotes, nil
},
getConfig: func() (gh.Config, error) {
cfg := &ghmock.ConfigMock{}
cfg.AuthenticationFunc = func() gh.AuthConfig {
authCfg := &config.AuthConfig{}
hosts := []string{"nonsense.com"}
if tt.override != "" {
hosts = append([]string{tt.override}, hosts...)
}
authCfg.SetHosts(hosts)
authCfg.SetActiveToken("", "")
authCfg.SetDefaultHost("nonsense.com", "hosts")
if tt.override != "" {
authCfg.SetDefaultHost(tt.override, "GH_HOST")
}
return authCfg
}
return cfg, nil
},
}
reg := &httpmock.Registry{}
defer reg.Verify(t)
ios, _, _, _ := iostreams.Test()
ios.SetStdinTTY(tt.tty)
ios.SetStdoutTTY(tt.tty)
if tt.httpStubs != nil {
tt.httpStubs(reg)
}
f.IOStreams = ios
f.HttpClient = func() (*http.Client, error) { return &http.Client{Transport: reg}, nil }
f.Remotes = rr.Resolver()
f.BaseRepo = SmartBaseRepoFunc(f)
repo, err := f.BaseRepo()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantsName, repo.RepoName())
assert.Equal(t, tt.wantsOwner, repo.RepoOwner())
assert.Equal(t, tt.wantsHost, repo.RepoHost())
})
}
}
// Defined in pkg/cmdutil/repo_override.go but test it along with other BaseRepo functions
func Test_OverrideBaseRepo(t *testing.T) {
tests := []struct {
name string
remotes git.RemoteSet
config gh.Config
envOverride string
argOverride string
wantsErr bool
wantsName string
wantsOwner string
wantsHost string
}{
{
name: "override from argument",
argOverride: "override/test",
wantsHost: "github.com",
wantsOwner: "override",
wantsName: "test",
},
{
name: "override from environment",
envOverride: "somehost.com/override/test",
wantsHost: "somehost.com",
wantsOwner: "override",
wantsName: "test",
},
{
name: "no override",
remotes: git.RemoteSet{
git.NewRemote("origin", "https://nonsense.com/owner/repo.git"),
},
config: defaultConfig(),
wantsHost: "nonsense.com",
wantsOwner: "owner",
wantsName: "repo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.envOverride != "" {
t.Setenv("GH_REPO", tt.envOverride)
}
f := New("1")
rr := &remoteResolver{
readRemotes: func() (git.RemoteSet, error) {
return tt.remotes, nil
},
getConfig: func() (gh.Config, error) {
return tt.config, nil
},
}
f.Remotes = rr.Resolver()
f.BaseRepo = cmdutil.OverrideBaseRepoFunc(f, tt.argOverride)
repo, err := f.BaseRepo()
if tt.wantsErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantsName, repo.RepoName())
assert.Equal(t, tt.wantsOwner, repo.RepoOwner())
assert.Equal(t, tt.wantsHost, repo.RepoHost())
})
}
}
func Test_ioStreams_pager(t *testing.T) {
tests := []struct {
name string
env map[string]string
config gh.Config
wantPager string
}{
{
name: "GH_PAGER and PAGER set",
env: map[string]string{
"GH_PAGER": "GH_PAGER",
"PAGER": "PAGER",
},
wantPager: "GH_PAGER",
},
{
name: "GH_PAGER and config pager set",
env: map[string]string{
"GH_PAGER": "GH_PAGER",
},
config: pagerConfig(),
wantPager: "GH_PAGER",
},
{
name: "config pager and PAGER set",
env: map[string]string{
"PAGER": "PAGER",
},
config: pagerConfig(),
wantPager: "CONFIG_PAGER",
},
{
name: "only PAGER set",
env: map[string]string{
"PAGER": "PAGER",
},
wantPager: "PAGER",
},
{
name: "GH_PAGER set to blank string",
env: map[string]string{
"GH_PAGER": "",
"PAGER": "PAGER",
},
wantPager: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.env != nil {
for k, v := range tt.env {
t.Setenv(k, v)
}
}
f := New("1")
f.Config = func() (gh.Config, error) {
if tt.config == nil {
return config.NewBlankConfig(), nil
} else {
return tt.config, nil
}
}
io := ioStreams(f)
assert.Equal(t, tt.wantPager, io.GetPager())
})
}
}
func Test_ioStreams_prompt(t *testing.T) {
tests := []struct {
name string
config gh.Config
promptDisabled bool
env map[string]string
}{
{
name: "default config",
promptDisabled: false,
},
{
name: "config with prompt disabled",
config: disablePromptConfig(),
promptDisabled: true,
},
{
name: "prompt disabled via GH_PROMPT_DISABLED env var",
env: map[string]string{"GH_PROMPT_DISABLED": "1"},
promptDisabled: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.env != nil {
for k, v := range tt.env {
t.Setenv(k, v)
}
}
f := New("1")
f.Config = func() (gh.Config, error) {
if tt.config == nil {
return config.NewBlankConfig(), nil
} else {
return tt.config, nil
}
}
io := ioStreams(f)
assert.Equal(t, tt.promptDisabled, io.GetNeverPrompt())
})
}
}
func Test_ioStreams_colorLabels(t *testing.T) {
tests := []struct {
name string
config gh.Config
colorLabelsEnabled bool
env map[string]string
}{
{
name: "default config",
colorLabelsEnabled: false,
},
{
name: "config with colorLabels enabled",
config: enableColorLabelsConfig(),
colorLabelsEnabled: true,
},
{
name: "colorLabels enabled via GH_COLOR_LABELS env var",
env: map[string]string{"GH_COLOR_LABELS": "1"},
colorLabelsEnabled: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.env != nil {
for k, v := range tt.env {
t.Setenv(k, v)
}
}
f := New("1")
f.Config = func() (gh.Config, error) {
if tt.config == nil {
return config.NewBlankConfig(), nil
} else {
return tt.config, nil
}
}
io := ioStreams(f)
assert.Equal(t, tt.colorLabelsEnabled, io.ColorLabels())
})
}
}
func TestSSOURL(t *testing.T) {
tests := []struct {
name string
host string
sso string
wantStderr string
wantSSO string
}{
{
name: "SSO challenge in response header",
host: "github.com",
sso: "required; url=https://github.com/login/sso?return_to=xyz&param=123abc; another",
wantStderr: "",
wantSSO: "https://github.com/login/sso?return_to=xyz&param=123abc",
},
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if sso := r.URL.Query().Get("sso"); sso != "" {
w.Header().Set("X-GitHub-SSO", sso)
}
w.WriteHeader(http.StatusNoContent)
}))
defer ts.Close()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := New("1")
f.Config = func() (gh.Config, error) {
return config.NewBlankConfig(), nil
}
ios, _, _, stderr := iostreams.Test()
f.IOStreams = ios
client, err := httpClientFunc(f, "v1.2.3")()
require.NoError(t, err)
req, err := http.NewRequest("GET", ts.URL, nil)
if tt.sso != "" {
q := req.URL.Query()
q.Set("sso", tt.sso)
req.URL.RawQuery = q.Encode()
}
req.Host = tt.host
require.NoError(t, err)
res, err := client.Do(req)
require.NoError(t, err)
assert.Equal(t, 204, res.StatusCode)
assert.Equal(t, tt.wantStderr, stderr.String())
assert.Equal(t, tt.wantSSO, SSOURL())
})
}
}
func TestNewGitClient(t *testing.T) {
tests := []struct {
name string
config gh.Config
executable string
wantAuthHosts []string
wantGhPath string
}{
{
name: "creates git client",
config: defaultConfig(),
executable: filepath.Join("path", "to", "gh"),
wantAuthHosts: []string{"nonsense.com"},
wantGhPath: filepath.Join("path", "to", "gh"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := New("1")
f.Config = func() (gh.Config, error) {
if tt.config == nil {
return config.NewBlankConfig(), nil
} else {
return tt.config, nil
}
}
f.ExecutableName = tt.executable
ios, _, _, _ := iostreams.Test()
f.IOStreams = ios
c := newGitClient(f)
assert.Equal(t, tt.wantGhPath, c.GhPath)
assert.Equal(t, ios.In, c.Stdin)
assert.Equal(t, ios.Out, c.Stdout)
assert.Equal(t, ios.ErrOut, c.Stderr)
})
}
}
func defaultConfig() *ghmock.ConfigMock {
cfg := config.NewFromString("")
cfg.Set("nonsense.com", "oauth_token", "BLAH")
return cfg
}
func pagerConfig() gh.Config {
return config.NewFromString("pager: CONFIG_PAGER")
}
func disablePromptConfig() gh.Config {
return config.NewFromString("prompt: disabled")
}
func enableColorLabelsConfig() gh.Config {
return config.NewFromString("color_labels: enabled")
}