Skip to content

Commit 2f729ee

Browse files
committed
test: confirm coverage matches actual behaviors
1 parent 87d927a commit 2f729ee

1 file changed

Lines changed: 290 additions & 58 deletions

File tree

internal/iostreams/prompts_test.go

Lines changed: 290 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
package iostreams
1616

1717
import (
18-
"fmt"
1918
"testing"
2019

2120
"github.com/slackapi/slack-cli/internal/config"
@@ -24,93 +23,326 @@ import (
2423
"github.com/slackapi/slack-cli/internal/slackerror"
2524
"github.com/spf13/pflag"
2625
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
2727
)
2828

29+
func TestPromptConfigs(t *testing.T) {
30+
mockFlag := &pflag.Flag{Name: "token"}
31+
mockFlag2 := &pflag.Flag{Name: "team"}
32+
33+
tests := map[string]struct {
34+
cfg PromptConfig
35+
required bool
36+
flags int
37+
}{
38+
"ConfirmPromptConfig required": {
39+
cfg: ConfirmPromptConfig{
40+
Required: true,
41+
},
42+
required: true,
43+
flags: 0,
44+
},
45+
"ConfirmPromptConfig optional": {
46+
cfg: ConfirmPromptConfig{
47+
Required: false,
48+
},
49+
required: false,
50+
flags: 0,
51+
},
52+
"InputPromptConfig required": {
53+
cfg: InputPromptConfig{
54+
Required: true,
55+
Placeholder: "hint",
56+
},
57+
required: true,
58+
flags: 0,
59+
},
60+
"InputPromptConfig optional": {
61+
cfg: InputPromptConfig{
62+
Required: false,
63+
},
64+
required: false,
65+
flags: 0,
66+
},
67+
"MultiSelectPromptConfig required": {
68+
cfg: MultiSelectPromptConfig{
69+
Required: true,
70+
},
71+
required: true,
72+
flags: 0,
73+
},
74+
"MultiSelectPromptConfig optional": {
75+
cfg: MultiSelectPromptConfig{
76+
Required: false,
77+
},
78+
required: false,
79+
flags: 0,
80+
},
81+
"PasswordPromptConfig with flag": {
82+
cfg: PasswordPromptConfig{
83+
Flag: mockFlag,
84+
Required: true,
85+
},
86+
required: true,
87+
flags: 1,
88+
},
89+
"PasswordPromptConfig without flag": {
90+
cfg: PasswordPromptConfig{
91+
Required: false,
92+
},
93+
required: false,
94+
flags: 0,
95+
},
96+
"SelectPromptConfig with single flag": {
97+
cfg: SelectPromptConfig{
98+
Flag: mockFlag,
99+
Required: true,
100+
},
101+
required: true,
102+
flags: 1,
103+
},
104+
"SelectPromptConfig with multiple flags": {
105+
cfg: SelectPromptConfig{
106+
Flags: []*pflag.Flag{mockFlag, mockFlag2},
107+
Required: true,
108+
},
109+
required: true,
110+
flags: 2,
111+
},
112+
"SelectPromptConfig without flags": {
113+
cfg: SelectPromptConfig{
114+
Required: false,
115+
},
116+
required: false,
117+
flags: 0,
118+
},
119+
}
120+
for name, tc := range tests {
121+
t.Run(name, func(t *testing.T) {
122+
assert.Equal(t, tc.required, tc.cfg.IsRequired())
123+
assert.Len(t, tc.cfg.GetFlags(), tc.flags)
124+
})
125+
}
126+
}
127+
128+
func TestRetrieveFlagValue(t *testing.T) {
129+
tests := map[string]struct {
130+
flagset []*pflag.Flag
131+
expectedFlag bool
132+
expectedError string
133+
}{
134+
"nil flagset returns nil": {
135+
flagset: nil,
136+
expectedFlag: false,
137+
},
138+
"nil flag in set is skipped": {
139+
flagset: []*pflag.Flag{nil},
140+
expectedFlag: false,
141+
},
142+
"unchanged flag returns nil": {
143+
flagset: []*pflag.Flag{
144+
{Name: "test", Changed: false},
145+
},
146+
expectedFlag: false,
147+
},
148+
"single changed flag is returned": {
149+
flagset: []*pflag.Flag{
150+
{Name: "test", Changed: true},
151+
},
152+
expectedFlag: true,
153+
},
154+
"multiple changed flags returns error": {
155+
flagset: []*pflag.Flag{
156+
{Name: "a", Changed: true},
157+
{Name: "b", Changed: true},
158+
},
159+
expectedError: slackerror.ErrMismatchedFlags,
160+
},
161+
}
162+
for name, tc := range tests {
163+
t.Run(name, func(t *testing.T) {
164+
io := &IOStreams{}
165+
flag, err := io.retrieveFlagValue(tc.flagset)
166+
if err != nil {
167+
assert.Nil(t, flag)
168+
assert.Equal(t, tc.expectedError, slackerror.ToSlackError(err).Code)
169+
} else {
170+
assert.NoError(t, err)
171+
assert.Equal(t, tc.expectedFlag, flag != nil)
172+
}
173+
})
174+
}
175+
}
176+
177+
func TestErrInteractivityFlags(t *testing.T) {
178+
tests := map[string]struct {
179+
cfg PromptConfig
180+
contains []string
181+
}{
182+
"no flags shows generic message": {
183+
cfg: ConfirmPromptConfig{},
184+
contains: []string{"not a TTY"},
185+
},
186+
"one flag suggests the flag name": {
187+
cfg: PasswordPromptConfig{
188+
Flag: &pflag.Flag{Name: "token"},
189+
},
190+
contains: []string{"--token"},
191+
},
192+
"multiple flags lists all flag names": {
193+
cfg: SelectPromptConfig{Flags: []*pflag.Flag{
194+
{Name: "app"},
195+
{Name: "team"},
196+
}},
197+
contains: []string{"--app", "--team"},
198+
},
199+
}
200+
for name, tc := range tests {
201+
t.Run(name, func(t *testing.T) {
202+
err := errInteractivityFlags(tc.cfg)
203+
for _, s := range tc.contains {
204+
assert.Contains(t, err.Error(), s)
205+
}
206+
})
207+
}
208+
}
209+
29210
func TestPasswordPrompt(t *testing.T) {
30211
tests := map[string]struct {
31-
FlagChanged bool
32-
FlagValue string
33-
Required bool
34-
IsInteractive bool
35-
ExpectedError *slackerror.Error
36-
ExpectedValue string
212+
flagChanged bool
213+
flagValue string
214+
required bool
215+
expectedError string
216+
expectedValue string
217+
expectedFlag bool
37218
}{
38219
"error if no flag is set": {
39-
IsInteractive: false,
40-
ExpectedError: slackerror.New(slackerror.ErrPrompt),
220+
expectedError: slackerror.ErrPrompt,
41221
},
42222
"error if the flag is missing a required value": {
43-
FlagChanged: true,
44-
FlagValue: "",
45-
Required: true,
46-
ExpectedError: slackerror.New(slackerror.ErrMissingFlag),
223+
flagChanged: true,
224+
flagValue: "",
225+
required: true,
226+
expectedError: slackerror.ErrMissingFlag,
47227
},
48228
"allow an empty flag value if not required": {
49-
FlagChanged: true,
50-
FlagValue: "",
51-
Required: false,
52-
ExpectedValue: "",
229+
flagChanged: true,
230+
flagValue: "",
231+
required: false,
232+
expectedValue: "",
233+
expectedFlag: true,
53234
},
54235
"use the provided flag value": {
55-
FlagChanged: true,
56-
FlagValue: "something secret",
57-
ExpectedValue: "something secret",
236+
flagChanged: true,
237+
flagValue: "something secret",
238+
expectedValue: "something secret",
239+
expectedFlag: true,
58240
},
59-
// "values can be entered interactively": {
60-
// IsInteractive: true,
61-
// },
62241
}
242+
for name, tc := range tests {
243+
t.Run(name, func(t *testing.T) {
244+
ctx := slackcontext.MockContext(t.Context())
63245

64-
var mockFlagValue string
65-
pflag.StringVar(&mockFlagValue, "mockedflag", "", "mock usage")
66-
mockFlag := pflag.Lookup("mockedflag")
246+
fsMock := slackdeps.NewFsMock()
247+
osMock := slackdeps.NewOsMock()
248+
// TODO: add interactive tests with FileInfoCharDevice for TTY stdout
249+
osMock.On("Stdout").Return(&slackdeps.FileMock{FileInfo: &slackdeps.FileInfoNamedPipe{}})
250+
cfg := config.NewConfig(fsMock, osMock)
251+
io := NewIOStreams(cfg, fsMock, osMock)
67252

68-
interactiveStdout := &slackdeps.FileMock{
69-
FileInfo: &slackdeps.FileInfoCharDevice{},
70-
}
71-
nonInteractiveStdout := &slackdeps.FileMock{
72-
FileInfo: &slackdeps.FileInfoNamedPipe{},
253+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
254+
fs.String("token", "", "")
255+
if tc.flagChanged {
256+
require.NoError(t, fs.Set("token", tc.flagValue))
257+
}
258+
flag := fs.Lookup("token")
259+
260+
selection, err := io.PasswordPrompt(ctx, "Enter a password", PasswordPromptConfig{
261+
Flag: flag,
262+
Required: tc.required,
263+
})
264+
265+
if err != nil {
266+
assert.Equal(t, tc.expectedError, slackerror.ToSlackError(err).Code)
267+
} else {
268+
assert.NoError(t, err)
269+
assert.Equal(t, tc.expectedValue, selection.Value)
270+
assert.Equal(t, tc.expectedFlag, selection.Flag)
271+
}
272+
})
73273
}
274+
}
74275

276+
func TestSelectPrompt(t *testing.T) {
277+
tests := map[string]struct {
278+
flagValue string
279+
flagChanged bool
280+
options []string
281+
required bool
282+
expectedError string
283+
expectedResp SelectPromptResponse
284+
}{
285+
"use provided flag value": {
286+
flagValue: "A123",
287+
flagChanged: true,
288+
options: []string{"A123", "A456"},
289+
required: true,
290+
expectedResp: SelectPromptResponse{
291+
Flag: true,
292+
Option: "A123",
293+
},
294+
},
295+
"error if required flag is empty": {
296+
flagValue: "",
297+
flagChanged: true,
298+
options: []string{"A123"},
299+
required: true,
300+
expectedError: slackerror.ErrMissingFlag,
301+
},
302+
"error if options are empty": {
303+
options: []string{},
304+
required: true,
305+
expectedError: slackerror.ErrMissingOptions,
306+
},
307+
"error if non-TTY and required": {
308+
options: []string{"a", "b"},
309+
required: true,
310+
expectedError: slackerror.ErrPrompt,
311+
},
312+
"no error if non-TTY and optional": {
313+
options: []string{"a", "b"},
314+
required: false,
315+
expectedResp: SelectPromptResponse{},
316+
},
317+
}
75318
for name, tc := range tests {
76319
t.Run(name, func(t *testing.T) {
77320
ctx := slackcontext.MockContext(t.Context())
321+
78322
fsMock := slackdeps.NewFsMock()
79323
osMock := slackdeps.NewOsMock()
80-
if tc.IsInteractive {
81-
osMock.On("Stdout").Return(interactiveStdout)
82-
} else {
83-
osMock.On("Stdout").Return(nonInteractiveStdout)
84-
}
85-
config := config.NewConfig(fsMock, osMock)
86-
ioStreams := NewIOStreams(config, fsMock, osMock)
324+
osMock.On("Stdout").Return(&slackdeps.FileMock{FileInfo: &slackdeps.FileInfoNamedPipe{}})
325+
cfg := config.NewConfig(fsMock, osMock)
326+
io := NewIOStreams(cfg, fsMock, osMock)
87327

88-
if tc.FlagChanged {
89-
mockFlag.Changed = true
90-
_ = mockFlag.Value.Set(tc.FlagValue)
91-
} else {
92-
mockFlag.Changed = false
93-
_ = mockFlag.Value.Set("")
328+
var flag *pflag.Flag
329+
if tc.flagChanged {
330+
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
331+
fs.String("app", "", "")
332+
require.NoError(t, fs.Set("app", tc.flagValue))
333+
flag = fs.Lookup("app")
94334
}
95335

96-
selection, err := ioStreams.PasswordPrompt(ctx, "Enter a password", PasswordPromptConfig{
97-
Flag: mockFlag,
98-
Required: tc.Required,
336+
selection, err := io.SelectPrompt(ctx, "Choose", tc.options, SelectPromptConfig{
337+
Flag: flag,
338+
Required: tc.required,
99339
})
100340

101-
if tc.ExpectedError != nil {
102-
assert.Equal(t, tc.ExpectedError.Code, slackerror.ToSlackError(err).Code)
103-
if tc.ExpectedError.Code == slackerror.ErrPrompt {
104-
assert.Contains(t, err.Error(), fmt.Sprintf("--%s", mockFlag.Name))
105-
}
341+
if err != nil {
342+
assert.Equal(t, tc.expectedError, slackerror.ToSlackError(err).Code)
106343
} else {
107344
assert.NoError(t, err)
108-
assert.Equal(t, selection.Value, tc.ExpectedValue)
109-
if tc.FlagChanged {
110-
assert.Equal(t, selection.Flag, true)
111-
} else {
112-
assert.Equal(t, selection.Prompt, true)
113-
}
345+
assert.Equal(t, tc.expectedResp, selection)
114346
}
115347
})
116348
}

0 commit comments

Comments
 (0)