11package cmd
22
33import (
4+ "bytes"
45 "context"
6+ "errors"
7+ "io"
58 "os"
69 "strings"
10+ "sync"
711 "testing"
812
13+ "github.com/spf13/afero"
914 "github.com/stretchr/testify/assert"
1015 "github.com/stretchr/testify/require"
1116
17+ "github.com/stripe/stripe-cli/pkg/config"
1218 "github.com/stripe/stripe-cli/pkg/plugins"
1319)
1420
15- func createPluginCmd () * pluginTemplateCmd {
21+ func createPluginCmd (cfg * config. Config ) * pluginTemplateCmd {
1622 plugin := plugins.Plugin {
1723 Shortname : "test" ,
1824 Shortdesc : "test your stuff" ,
@@ -26,7 +32,7 @@ func createPluginCmd() *pluginTemplateCmd {
2632 }},
2733 }
2834
29- pluginCmd := newPluginTemplateCmd (& Config , & plugin )
35+ pluginCmd := newPluginTemplateCmd (cfg , & plugin )
3036
3137 return pluginCmd
3238}
@@ -35,7 +41,8 @@ func createPluginCmd() *pluginTemplateCmd {
3541// This is a complex dance between the CLI itself and the plugin, so the flags come from
3642// two different sources as a result. This test is here to catch any non-obvious regressions
3743func TestFlagsArePassedAsArgs (t * testing.T ) {
38- pluginCmd := createPluginCmd ()
44+ cfg := & config.Config {}
45+ pluginCmd := createPluginCmd (cfg )
3946 rootCmd .AddCommand (pluginCmd .cmd )
4047
4148 Execute (context .Background ())
@@ -133,6 +140,105 @@ func TestAddPluginSubcommandStubsSkipsEmptyName(t *testing.T) {
133140 assert .Equal (t , "valid" , cmds [1 ].Name ())
134141}
135142
143+ // TestWithBackgroundUpdate_FnRuns verifies that fn is invoked and completes.
144+ func TestWithBackgroundUpdate_FnRuns (t * testing.T ) {
145+ cfg := & config.Config {}
146+ fs := afero .NewMemMapFs ()
147+ plugin := plugins.Plugin {Shortname : "test" }
148+
149+ called := false
150+ err := plugins .WithBackgroundUpdate (context .Background (), cfg , fs , "" , & plugin , io .Discard , func () error {
151+ called = true
152+ return nil
153+ })
154+
155+ assert .NoError (t , err )
156+ assert .True (t , called )
157+ }
158+
159+ // TestWithBackgroundUpdate_PropagatesError verifies that an error returned by fn
160+ // is returned by WithBackgroundUpdate.
161+ func TestWithBackgroundUpdate_PropagatesError (t * testing.T ) {
162+ cfg := & config.Config {}
163+ fs := afero .NewMemMapFs ()
164+ plugin := plugins.Plugin {Shortname : "test" }
165+
166+ want := errors .New ("plugin failed" )
167+ err := plugins .WithBackgroundUpdate (context .Background (), cfg , fs , "" , & plugin , io .Discard , func () error {
168+ return want
169+ })
170+
171+ assert .Equal (t , want , err )
172+ }
173+
174+ // TestWithBackgroundUpdate_UpdateOutputAppearsAfterFn verifies that any output
175+ // written by the background update goroutine is only flushed to the underlying
176+ // writer after fn returns — never interleaved with fn's execution.
177+ func TestWithBackgroundUpdate_UpdateOutputAppearsAfterFn (t * testing.T ) {
178+ cfg := & config.Config {}
179+ fs := afero .NewMemMapFs ()
180+ plugin := plugins.Plugin {Shortname : "test" }
181+
182+ var mu sync.Mutex
183+ var events []string
184+
185+ // recordWriter appends each Write as an event.
186+ out := & funcWriter {fn : func (p []byte ) (int , error ) {
187+ mu .Lock ()
188+ events = append (events , "write:" + string (p ))
189+ mu .Unlock ()
190+ return len (p ), nil
191+ }}
192+
193+ err := plugins .WithBackgroundUpdate (context .Background (), cfg , fs , "" , & plugin , out , func () error {
194+ mu .Lock ()
195+ events = append (events , "fn:done" )
196+ mu .Unlock ()
197+ return nil
198+ })
199+
200+ require .NoError (t , err )
201+
202+ mu .Lock ()
203+ defer mu .Unlock ()
204+ // If there were any writes (update output), they must come after fn:done.
205+ fnIdx := - 1
206+ for i , e := range events {
207+ if e == "fn:done" {
208+ fnIdx = i
209+ }
210+ }
211+ // fn must have run
212+ require .GreaterOrEqual (t , fnIdx , 0 , "fn:done not recorded" )
213+ for i , e := range events {
214+ if strings .HasPrefix (e , "write:" ) {
215+ assert .Greater (t , i , fnIdx , "update write at index %d appeared before fn:done at index %d" , i , fnIdx )
216+ }
217+ }
218+ }
219+
220+ // TestWithBackgroundUpdate_NilErrorOnSuccess verifies a nil error on clean fn exit.
221+ func TestWithBackgroundUpdate_NilErrorOnSuccess (t * testing.T ) {
222+ cfg := & config.Config {}
223+ fs := afero .NewMemMapFs ()
224+ plugin := plugins.Plugin {Shortname : "test" }
225+
226+ // Provide a non-nil out to ensure the writer path is exercised.
227+ var buf bytes.Buffer
228+ err := plugins .WithBackgroundUpdate (context .Background (), cfg , fs , "" , & plugin , & buf , func () error {
229+ return nil
230+ })
231+
232+ assert .NoError (t , err )
233+ }
234+
235+ // funcWriter is a minimal io.Writer backed by a function, used in tests.
236+ type funcWriter struct {
237+ fn func ([]byte ) (int , error )
238+ }
239+
240+ func (f * funcWriter ) Write (p []byte ) (int , error ) { return f .fn (p ) }
241+
136242func TestSubsliceAfter (t * testing.T ) {
137243 tests := []struct {
138244 name string
0 commit comments