899 lines
24 KiB
Go
899 lines
24 KiB
Go
package set
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
ghContext "github.com/cli/cli/v2/context"
|
|
"github.com/cli/cli/v2/git"
|
|
"github.com/cli/cli/v2/internal/config"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
"github.com/cli/cli/v2/pkg/cmd/secret/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 TestNewCmdSet(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args string
|
|
wants SetOptions
|
|
stdinTTY bool
|
|
wantsErr bool
|
|
wantsErrMessage string
|
|
}{
|
|
{
|
|
name: "invalid visibility",
|
|
args: "cool_secret --org coolOrg -v mistyVeil",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "when visibility is selected, requires indication of repos",
|
|
args: "cool_secret --org coolOrg -v selected",
|
|
wantsErr: true,
|
|
wantsErrMessage: "`--repos` or `--no-repos-selected` required with `--visibility=selected`",
|
|
},
|
|
{
|
|
name: "visibilities other than selected do not accept --repos",
|
|
args: "cool_secret --org coolOrg -v private -r coolRepo",
|
|
wantsErr: true,
|
|
wantsErrMessage: "`--repos` is only supported with `--visibility=selected`",
|
|
},
|
|
{
|
|
name: "visibilities other than selected do not accept --no-repos-selected",
|
|
args: "cool_secret --org coolOrg -v private --no-repos-selected",
|
|
wantsErr: true,
|
|
wantsErrMessage: "`--no-repos-selected` is only supported with `--visibility=selected`",
|
|
},
|
|
{
|
|
name: "--repos and --no-repos-selected are mutually exclusive",
|
|
args: `--repos coolRepo --no-repos-selected cool_secret`,
|
|
wantsErr: true,
|
|
wantsErrMessage: "specify only one of `--repos` or `--no-repos-selected`",
|
|
},
|
|
{
|
|
name: "secret name is required",
|
|
args: "",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "multiple positional arguments are not allowed",
|
|
args: "cool_secret good_secret",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "visibility is only allowed with --org",
|
|
args: "cool_secret -v all",
|
|
wantsErr: true,
|
|
},
|
|
{
|
|
name: "providing --repos without --visibility implies selected visibility",
|
|
args: "cool_secret --body secret-body --org coolOrg --repos coolRepo",
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{"coolRepo"},
|
|
Body: "secret-body",
|
|
OrgName: "coolOrg",
|
|
},
|
|
},
|
|
{
|
|
name: "providing --no-repos-selected without --visibility implies selected visibility",
|
|
args: "cool_secret --body secret-body --org coolOrg --no-repos-selected",
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{},
|
|
Body: "secret-body",
|
|
OrgName: "coolOrg",
|
|
},
|
|
},
|
|
{
|
|
name: "org with selected repo",
|
|
args: "-o coolOrg --body secret-body -v selected -r coolRepo cool_secret",
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{"coolRepo"},
|
|
Body: "secret-body",
|
|
OrgName: "coolOrg",
|
|
},
|
|
},
|
|
{
|
|
name: "org with selected repos",
|
|
args: `--org coolOrg --body secret-body -v selected --repos "coolRepo,radRepo,goodRepo" cool_secret`,
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{"coolRepo", "goodRepo", "radRepo"},
|
|
Body: "secret-body",
|
|
OrgName: "coolOrg",
|
|
},
|
|
},
|
|
{
|
|
name: "user with selected repos",
|
|
args: `-u --body secret-body -r "monalisa/coolRepo,cli/cli,github/hub" cool_secret`,
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{"monalisa/coolRepo", "cli/cli", "github/hub"},
|
|
Body: "secret-body",
|
|
},
|
|
},
|
|
{
|
|
name: "--user is mutually exclusive with --no-repos-selected",
|
|
args: `-u --no-repos-selected cool_secret`,
|
|
wantsErr: true,
|
|
wantsErrMessage: "`--no-repos-selected` must be omitted when used with `--user`",
|
|
},
|
|
{
|
|
name: "repo",
|
|
args: `cool_secret --body "a secret"`,
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Private,
|
|
Body: "a secret",
|
|
OrgName: "",
|
|
},
|
|
},
|
|
{
|
|
name: "env",
|
|
args: `cool_secret --body "a secret" --env Release`,
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Private,
|
|
Body: "a secret",
|
|
OrgName: "",
|
|
EnvName: "Release",
|
|
},
|
|
},
|
|
{
|
|
name: "vis all",
|
|
args: `cool_secret --org coolOrg --body "cool" --visibility all`,
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.All,
|
|
Body: "cool",
|
|
OrgName: "coolOrg",
|
|
},
|
|
},
|
|
{
|
|
name: "no store",
|
|
args: `cool_secret --no-store`,
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Private,
|
|
DoNotStore: true,
|
|
},
|
|
},
|
|
{
|
|
name: "Dependabot repo",
|
|
args: `cool_secret --body "a secret" --app Dependabot`,
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Private,
|
|
Body: "a secret",
|
|
OrgName: "",
|
|
Application: "Dependabot",
|
|
},
|
|
},
|
|
{
|
|
name: "Dependabot org",
|
|
args: "--org coolOrg --body secret-body --visibility selected --repos coolRepo cool_secret --app Dependabot",
|
|
wants: SetOptions{
|
|
SecretName: "cool_secret",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{"coolRepo"},
|
|
Body: "secret-body",
|
|
OrgName: "coolOrg",
|
|
Application: "Dependabot",
|
|
},
|
|
},
|
|
{
|
|
name: "Codespaces org",
|
|
args: `random_secret --org coolOrg --body "random value" --visibility selected --repos "coolRepo,cli/cli" --app Codespaces`,
|
|
wants: SetOptions{
|
|
SecretName: "random_secret",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{"coolRepo", "cli/cli"},
|
|
Body: "random value",
|
|
OrgName: "coolOrg",
|
|
Application: "Codespaces",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ios, _, _, _ := iostreams.Test()
|
|
f := &cmdutil.Factory{
|
|
IOStreams: ios,
|
|
}
|
|
|
|
ios.SetStdinTTY(tt.stdinTTY)
|
|
|
|
argv, err := shlex.Split(tt.args)
|
|
assert.NoError(t, err)
|
|
|
|
var gotOpts *SetOptions
|
|
cmd := NewCmdSet(f, func(opts *SetOptions) 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)
|
|
if tt.wantsErrMessage != "" {
|
|
assert.EqualError(t, err, tt.wantsErrMessage)
|
|
}
|
|
return
|
|
}
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.wants.SecretName, gotOpts.SecretName)
|
|
assert.Equal(t, tt.wants.Body, gotOpts.Body)
|
|
assert.Equal(t, tt.wants.Visibility, gotOpts.Visibility)
|
|
assert.Equal(t, tt.wants.OrgName, gotOpts.OrgName)
|
|
assert.Equal(t, tt.wants.EnvName, gotOpts.EnvName)
|
|
assert.Equal(t, tt.wants.DoNotStore, gotOpts.DoNotStore)
|
|
assert.ElementsMatch(t, tt.wants.RepositoryNames, gotOpts.RepositoryNames)
|
|
assert.Equal(t, tt.wants.Application, gotOpts.Application)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewCmdSetBaseRepoFuncs(t *testing.T) {
|
|
multipleRemotes := ghContext.Remotes{
|
|
&ghContext.Remote{
|
|
Remote: &git.Remote{
|
|
Name: "origin",
|
|
},
|
|
Repo: ghrepo.New("owner", "fork"),
|
|
},
|
|
&ghContext.Remote{
|
|
Remote: &git.Remote{
|
|
Name: "upstream",
|
|
},
|
|
Repo: ghrepo.New("owner", "repo"),
|
|
},
|
|
}
|
|
|
|
singleRemote := ghContext.Remotes{
|
|
&ghContext.Remote{
|
|
Remote: &git.Remote{
|
|
Name: "origin",
|
|
},
|
|
Repo: ghrepo.New("owner", "repo"),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args string
|
|
env map[string]string
|
|
remotes ghContext.Remotes
|
|
prompterStubs func(*prompter.MockPrompter)
|
|
wantRepo ghrepo.Interface
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "when there is a repo flag provided, the factory base repo func is used",
|
|
args: "SECRET_NAME --repo owner/repo",
|
|
remotes: multipleRemotes,
|
|
wantRepo: ghrepo.New("owner", "repo"),
|
|
},
|
|
{
|
|
name: "when GH_REPO env var is provided, the factory base repo func is used",
|
|
args: "SECRET_NAME",
|
|
env: map[string]string{
|
|
"GH_REPO": "owner/repo",
|
|
},
|
|
remotes: multipleRemotes,
|
|
wantRepo: ghrepo.New("owner", "repo"),
|
|
},
|
|
{
|
|
name: "when there is no repo flag or GH_REPO env var provided, and no prompting, the base func requiring no ambiguity is used",
|
|
args: "SECRET_NAME",
|
|
remotes: multipleRemotes,
|
|
wantErr: shared.AmbiguousBaseRepoError{
|
|
Remotes: multipleRemotes,
|
|
},
|
|
},
|
|
{
|
|
name: "when there is no repo flag or GH_REPO env provided, and there is a single remote, the factory base repo func is used",
|
|
args: "SECRET_NAME",
|
|
remotes: singleRemote,
|
|
wantRepo: ghrepo.New("owner", "repo"),
|
|
},
|
|
{
|
|
name: "when there is no repo flag or GH_REPO env var provided, and can prompt, the base func resolving ambiguity is used",
|
|
args: "SECRET_NAME",
|
|
remotes: multipleRemotes,
|
|
prompterStubs: func(pm *prompter.MockPrompter) {
|
|
pm.RegisterSelect(
|
|
"Select a repo",
|
|
[]string{"owner/fork", "owner/repo"},
|
|
func(_, _ string, opts []string) (int, error) {
|
|
return prompter.IndexFor(opts, "owner/fork")
|
|
},
|
|
)
|
|
},
|
|
wantRepo: ghrepo.New("owner", "fork"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ios, _, _, _ := iostreams.Test()
|
|
var pm *prompter.MockPrompter
|
|
if tt.prompterStubs != nil {
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStdoutTTY(true)
|
|
ios.SetStderrTTY(true)
|
|
pm = prompter.NewMockPrompter(t)
|
|
tt.prompterStubs(pm)
|
|
}
|
|
|
|
f := &cmdutil.Factory{
|
|
IOStreams: ios,
|
|
BaseRepo: func() (ghrepo.Interface, error) {
|
|
return ghrepo.FromFullName("owner/repo")
|
|
},
|
|
Prompter: pm,
|
|
Remotes: func() (ghContext.Remotes, error) {
|
|
return tt.remotes, nil
|
|
},
|
|
}
|
|
|
|
for k, v := range tt.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
|
|
argv, err := shlex.Split(tt.args)
|
|
assert.NoError(t, err)
|
|
|
|
var gotOpts *SetOptions
|
|
cmd := NewCmdSet(f, func(opts *SetOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
// Require to support --repo flag
|
|
cmdutil.EnableRepoOverride(cmd, f)
|
|
cmd.SetArgs(argv)
|
|
cmd.SetIn(&bytes.Buffer{})
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
|
|
_, err = cmd.ExecuteC()
|
|
require.NoError(t, err)
|
|
|
|
baseRepo, err := gotOpts.BaseRepo()
|
|
if tt.wantErr != nil {
|
|
require.Equal(t, tt.wantErr, err)
|
|
return
|
|
}
|
|
require.True(t, ghrepo.IsSame(tt.wantRepo, baseRepo))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_setRun_repo(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opts *SetOptions
|
|
wantApp string
|
|
}{
|
|
{
|
|
name: "Actions",
|
|
opts: &SetOptions{
|
|
Application: "actions",
|
|
},
|
|
wantApp: "actions",
|
|
},
|
|
{
|
|
name: "Dependabot",
|
|
opts: &SetOptions{
|
|
Application: "dependabot",
|
|
},
|
|
wantApp: "dependabot",
|
|
},
|
|
{
|
|
name: "defaults to Actions",
|
|
opts: &SetOptions{
|
|
Application: "",
|
|
},
|
|
wantApp: "actions",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
|
|
reg.Register(httpmock.REST("GET", fmt.Sprintf("repos/owner/repo/%s/secrets/public-key", tt.wantApp)),
|
|
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
|
|
|
|
reg.Register(httpmock.REST("PUT", fmt.Sprintf("repos/owner/repo/%s/secrets/cool_secret", tt.wantApp)),
|
|
httpmock.StatusStringResponse(201, `{}`))
|
|
|
|
ios, _, _, _ := iostreams.Test()
|
|
|
|
opts := &SetOptions{
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
BaseRepo: func() (ghrepo.Interface, error) {
|
|
return ghrepo.FromFullName("owner/repo")
|
|
},
|
|
IO: ios,
|
|
SecretName: "cool_secret",
|
|
Body: "a secret",
|
|
RandomOverride: fakeRandom,
|
|
Application: tt.opts.Application,
|
|
}
|
|
|
|
err := setRun(opts)
|
|
assert.NoError(t, err)
|
|
|
|
reg.Verify(t)
|
|
|
|
data, err := io.ReadAll(reg.Requests[1].Body)
|
|
assert.NoError(t, err)
|
|
var payload SecretPayload
|
|
err = json.Unmarshal(data, &payload)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, payload.KeyID, "123")
|
|
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_setRun_env(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
|
|
reg.Register(httpmock.REST("GET", "repos/owner/repo/environments/development/secrets/public-key"),
|
|
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
|
|
|
|
reg.Register(httpmock.REST("PUT", "repos/owner/repo/environments/development/secrets/cool_secret"), httpmock.StatusStringResponse(201, `{}`))
|
|
|
|
ios, _, _, _ := iostreams.Test()
|
|
|
|
opts := &SetOptions{
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
|
|
BaseRepo: func() (ghrepo.Interface, error) {
|
|
return ghrepo.FromFullName("owner/repo")
|
|
},
|
|
EnvName: "development",
|
|
IO: ios,
|
|
SecretName: "cool_secret",
|
|
Body: "a secret",
|
|
RandomOverride: fakeRandom,
|
|
}
|
|
|
|
err := setRun(opts)
|
|
assert.NoError(t, err)
|
|
|
|
reg.Verify(t)
|
|
|
|
data, err := io.ReadAll(reg.Requests[1].Body)
|
|
assert.NoError(t, err)
|
|
var payload SecretPayload
|
|
err = json.Unmarshal(data, &payload)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, payload.KeyID, "123")
|
|
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
|
|
}
|
|
|
|
func Test_setRun_org(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opts *SetOptions
|
|
wantVisibility shared.Visibility
|
|
wantRepositories []int64
|
|
wantDependabotRepositories []string
|
|
wantApp string
|
|
}{
|
|
{
|
|
name: "all vis",
|
|
opts: &SetOptions{
|
|
OrgName: "UmbrellaCorporation",
|
|
Visibility: shared.All,
|
|
},
|
|
wantApp: "actions",
|
|
},
|
|
{
|
|
name: "selected visibility",
|
|
opts: &SetOptions{
|
|
OrgName: "UmbrellaCorporation",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"},
|
|
},
|
|
wantRepositories: []int64{1, 2},
|
|
wantApp: "actions",
|
|
},
|
|
{
|
|
name: "no repos visibility",
|
|
opts: &SetOptions{
|
|
OrgName: "UmbrellaCorporation",
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{},
|
|
},
|
|
wantRepositories: []int64{},
|
|
wantApp: "actions",
|
|
},
|
|
{
|
|
name: "Dependabot",
|
|
opts: &SetOptions{
|
|
OrgName: "UmbrellaCorporation",
|
|
Visibility: shared.All,
|
|
Application: shared.Dependabot,
|
|
},
|
|
wantApp: "dependabot",
|
|
},
|
|
{
|
|
name: "Dependabot selected visibility",
|
|
opts: &SetOptions{
|
|
OrgName: "UmbrellaCorporation",
|
|
Visibility: shared.Selected,
|
|
Application: shared.Dependabot,
|
|
RepositoryNames: []string{"birkin", "UmbrellaCorporation/wesker"},
|
|
},
|
|
wantDependabotRepositories: []string{"1", "2"},
|
|
wantApp: "dependabot",
|
|
},
|
|
{
|
|
name: "Dependabot no repos visibility",
|
|
opts: &SetOptions{
|
|
OrgName: "UmbrellaCorporation",
|
|
Visibility: shared.Selected,
|
|
Application: shared.Dependabot,
|
|
RepositoryNames: []string{},
|
|
},
|
|
wantRepositories: []int64{},
|
|
wantApp: "dependabot",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
|
|
orgName := tt.opts.OrgName
|
|
|
|
reg.Register(httpmock.REST("GET",
|
|
fmt.Sprintf("orgs/%s/%s/secrets/public-key", orgName, tt.wantApp)),
|
|
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
|
|
|
|
reg.Register(httpmock.REST("PUT",
|
|
fmt.Sprintf("orgs/%s/%s/secrets/cool_secret", orgName, tt.wantApp)),
|
|
httpmock.StatusStringResponse(201, `{}`))
|
|
|
|
if len(tt.opts.RepositoryNames) > 0 {
|
|
reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
|
|
httpmock.StringResponse(`{"data":{"repo_0001":{"databaseId":1},"repo_0002":{"databaseId":2}}}`))
|
|
}
|
|
|
|
ios, _, _, _ := iostreams.Test()
|
|
|
|
tt.opts.BaseRepo = func() (ghrepo.Interface, error) {
|
|
return ghrepo.FromFullName("owner/repo")
|
|
}
|
|
tt.opts.HttpClient = func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
}
|
|
tt.opts.Config = func() (gh.Config, error) {
|
|
return config.NewBlankConfig(), nil
|
|
}
|
|
tt.opts.IO = ios
|
|
tt.opts.SecretName = "cool_secret"
|
|
tt.opts.Body = "a secret"
|
|
tt.opts.RandomOverride = fakeRandom
|
|
|
|
err := setRun(tt.opts)
|
|
assert.NoError(t, err)
|
|
|
|
reg.Verify(t)
|
|
|
|
data, err := io.ReadAll(reg.Requests[len(reg.Requests)-1].Body)
|
|
assert.NoError(t, err)
|
|
|
|
if tt.opts.Application == shared.Dependabot {
|
|
var payload DependabotSecretPayload
|
|
err = json.Unmarshal(data, &payload)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, payload.KeyID, "123")
|
|
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
|
|
assert.Equal(t, payload.Visibility, tt.opts.Visibility)
|
|
assert.ElementsMatch(t, payload.Repositories, tt.wantDependabotRepositories)
|
|
} else {
|
|
var payload SecretPayload
|
|
err = json.Unmarshal(data, &payload)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, payload.KeyID, "123")
|
|
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
|
|
assert.Equal(t, payload.Visibility, tt.opts.Visibility)
|
|
assert.ElementsMatch(t, payload.Repositories, tt.wantRepositories)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_setRun_user(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opts *SetOptions
|
|
wantVisibility shared.Visibility
|
|
wantRepositories []int64
|
|
}{
|
|
{
|
|
name: "all vis",
|
|
opts: &SetOptions{
|
|
UserSecrets: true,
|
|
Visibility: shared.All,
|
|
},
|
|
},
|
|
{
|
|
name: "selected visibility",
|
|
opts: &SetOptions{
|
|
UserSecrets: true,
|
|
Visibility: shared.Selected,
|
|
RepositoryNames: []string{"cli/cli", "github/hub"},
|
|
},
|
|
wantRepositories: []int64{212613049, 401025},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
|
|
reg.Register(httpmock.REST("GET", "user/codespaces/secrets/public-key"),
|
|
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
|
|
|
|
reg.Register(httpmock.REST("PUT", "user/codespaces/secrets/cool_secret"),
|
|
httpmock.StatusStringResponse(201, `{}`))
|
|
|
|
if len(tt.opts.RepositoryNames) > 0 {
|
|
reg.Register(httpmock.GraphQL(`query MapRepositoryNames\b`),
|
|
httpmock.StringResponse(`{"data":{"repo_0001":{"databaseId":212613049},"repo_0002":{"databaseId":401025}}}`))
|
|
}
|
|
|
|
ios, _, _, _ := iostreams.Test()
|
|
|
|
tt.opts.HttpClient = func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
}
|
|
tt.opts.Config = func() (gh.Config, error) {
|
|
return config.NewBlankConfig(), nil
|
|
}
|
|
tt.opts.IO = ios
|
|
tt.opts.SecretName = "cool_secret"
|
|
tt.opts.Body = "a secret"
|
|
tt.opts.RandomOverride = fakeRandom
|
|
|
|
err := setRun(tt.opts)
|
|
assert.NoError(t, err)
|
|
|
|
reg.Verify(t)
|
|
|
|
data, err := io.ReadAll(reg.Requests[len(reg.Requests)-1].Body)
|
|
assert.NoError(t, err)
|
|
var payload SecretPayload
|
|
err = json.Unmarshal(data, &payload)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, payload.KeyID, "123")
|
|
assert.Equal(t, payload.EncryptedValue, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=")
|
|
assert.ElementsMatch(t, payload.Repositories, tt.wantRepositories)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_setRun_shouldNotStore(t *testing.T) {
|
|
reg := &httpmock.Registry{}
|
|
defer reg.Verify(t)
|
|
|
|
reg.Register(httpmock.REST("GET", "repos/owner/repo/actions/secrets/public-key"),
|
|
httpmock.JSONResponse(PubKey{ID: "123", Key: "CDjXqf7AJBXWhMczcy+Fs7JlACEptgceysutztHaFQI="}))
|
|
|
|
ios, _, stdout, stderr := iostreams.Test()
|
|
|
|
opts := &SetOptions{
|
|
HttpClient: func() (*http.Client, error) {
|
|
return &http.Client{Transport: reg}, nil
|
|
},
|
|
Config: func() (gh.Config, error) {
|
|
return config.NewBlankConfig(), nil
|
|
},
|
|
BaseRepo: func() (ghrepo.Interface, error) {
|
|
return ghrepo.FromFullName("owner/repo")
|
|
},
|
|
IO: ios,
|
|
Body: "a secret",
|
|
DoNotStore: true,
|
|
RandomOverride: fakeRandom,
|
|
}
|
|
|
|
err := setRun(opts)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "UKYUCbHd0DJemxa3AOcZ6XcsBwALG9d4bpB8ZT0gSV39vl3BHiGSgj8zJapDxgB2BwqNqRhpjC4=\n", stdout.String())
|
|
assert.Equal(t, "", stderr.String())
|
|
}
|
|
|
|
func Test_getBody(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
bodyArg string
|
|
want string
|
|
stdin string
|
|
}{
|
|
{
|
|
name: "literal value",
|
|
bodyArg: "a secret",
|
|
want: "a secret",
|
|
},
|
|
{
|
|
name: "from stdin",
|
|
want: "a secret",
|
|
stdin: "a secret",
|
|
},
|
|
{
|
|
name: "from stdin with trailing newline character",
|
|
want: "a secret",
|
|
stdin: "a secret\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ios, stdin, _, _ := iostreams.Test()
|
|
|
|
ios.SetStdinTTY(false)
|
|
|
|
_, err := stdin.WriteString(tt.stdin)
|
|
assert.NoError(t, err)
|
|
|
|
body, err := getBody(&SetOptions{
|
|
Body: tt.bodyArg,
|
|
IO: ios,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.want, string(body))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_getBodyPrompt(t *testing.T) {
|
|
ios, _, _, _ := iostreams.Test()
|
|
ios.SetStdinTTY(true)
|
|
ios.SetStdoutTTY(true)
|
|
|
|
pm := prompter.NewMockPrompter(t)
|
|
pm.RegisterPassword("Paste your secret:", func(_ string) (string, error) {
|
|
return "cool secret", nil
|
|
})
|
|
|
|
body, err := getBody(&SetOptions{
|
|
IO: ios,
|
|
Prompter: pm,
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, string(body), "cool secret")
|
|
}
|
|
|
|
func Test_getSecretsFromOptions(t *testing.T) {
|
|
genFile := func(s string) string {
|
|
f, err := os.CreateTemp("", "gh-env.*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
return ""
|
|
}
|
|
defer f.Close()
|
|
t.Cleanup(func() {
|
|
_ = os.Remove(f.Name())
|
|
})
|
|
_, err = f.WriteString(s)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return f.Name()
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
opts SetOptions
|
|
isTTY bool
|
|
stdin string
|
|
want map[string]string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "secret from arg",
|
|
opts: SetOptions{
|
|
SecretName: "FOO",
|
|
Body: "bar",
|
|
EnvFile: "",
|
|
},
|
|
want: map[string]string{"FOO": "bar"},
|
|
},
|
|
{
|
|
name: "secrets from stdin",
|
|
opts: SetOptions{
|
|
Body: "",
|
|
EnvFile: "-",
|
|
},
|
|
stdin: `FOO=bar`,
|
|
want: map[string]string{"FOO": "bar"},
|
|
},
|
|
{
|
|
name: "secrets from file",
|
|
opts: SetOptions{
|
|
Body: "",
|
|
EnvFile: genFile(heredoc.Doc(`
|
|
FOO=bar
|
|
QUOTED="my value"
|
|
#IGNORED=true
|
|
export SHELL=bash
|
|
`)),
|
|
},
|
|
stdin: `FOO=bar`,
|
|
want: map[string]string{
|
|
"FOO": "bar",
|
|
"SHELL": "bash",
|
|
"QUOTED": "my value",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ios, stdin, _, _ := iostreams.Test()
|
|
ios.SetStdinTTY(tt.isTTY)
|
|
ios.SetStdoutTTY(tt.isTTY)
|
|
stdin.WriteString(tt.stdin)
|
|
opts := tt.opts
|
|
opts.IO = ios
|
|
gotSecrets, err := getSecretsFromOptions(&opts)
|
|
if err != nil {
|
|
if !tt.wantErr {
|
|
t.Fatalf("getSecretsFromOptions() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
} else if tt.wantErr {
|
|
t.Fatalf("getSecretsFromOptions() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if len(gotSecrets) != len(tt.want) {
|
|
t.Fatalf("getSecretsFromOptions() = got %d secrets, want %d", len(gotSecrets), len(tt.want))
|
|
}
|
|
for k, v := range gotSecrets {
|
|
if tt.want[k] != string(v) {
|
|
t.Errorf("getSecretsFromOptions() %s = got %q, want %q", k, string(v), tt.want[k])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func fakeRandom() io.Reader {
|
|
return bytes.NewReader(bytes.Repeat([]byte{5}, 32))
|
|
}
|