@@ -8,9 +8,11 @@ import (
88 "github.com/stretchr/testify/assert"
99 "github.com/stretchr/testify/require"
1010 fly "github.com/superfly/fly-go"
11+ "github.com/superfly/flyctl/internal/appconfig"
1112 "github.com/superfly/flyctl/internal/flag/flagctx"
1213 "github.com/superfly/flyctl/internal/flyutil"
1314 "github.com/superfly/flyctl/internal/mock"
15+ "github.com/superfly/flyctl/iostreams"
1416)
1517
1618func newDetermineOrgCtx (t * testing.T , orgFlag string ) context.Context {
@@ -112,3 +114,80 @@ func TestDetermineOrg(t *testing.T) {
112114 assert .Equal (t , "personal" , org .Slug )
113115 })
114116}
117+
118+ // newDetermineBaseAppConfigCtx builds a context wired with the flags that
119+ // determineBaseAppConfig reads. Pass configPath="" to leave --config unset.
120+ func newDetermineBaseAppConfigCtx (t * testing.T , copyConfigFlag , explicitConfigPath bool ) context.Context {
121+ t .Helper ()
122+
123+ ctx := context .Background ()
124+ ctx = iostreams .NewContext (ctx , iostreams .System ())
125+
126+ flagSet := pflag .NewFlagSet ("test" , pflag .ContinueOnError )
127+ flagSet .String ("config" , "" , "" )
128+ flagSet .Bool ("copy-config" , false , "" )
129+ flagSet .Bool ("attach" , false , "" )
130+ flagSet .Bool ("yes" , false , "" )
131+
132+ if copyConfigFlag {
133+ require .NoError (t , flagSet .Set ("copy-config" , "true" ))
134+ }
135+ if explicitConfigPath {
136+ require .NoError (t , flagSet .Set ("config" , "fly.custom.toml" ))
137+ }
138+
139+ return flagctx .NewContext (ctx , flagSet )
140+ }
141+
142+ func TestDetermineBaseAppConfig (t * testing.T ) {
143+ // existingCfg simulates what LoadAppConfigIfPresent puts in context when
144+ // the customer has a custom fly.toml with a non-default dockerfile.
145+ existingCfg := appconfig .NewConfig ()
146+ existingCfg .Build = & appconfig.Build {
147+ Dockerfile : "docker.ui-server.dockerfile" ,
148+ }
149+
150+ t .Run ("no flags and no existing config returns blank config" , func (t * testing.T ) {
151+ ctx := newDetermineBaseAppConfigCtx (t , false , false )
152+ // No config in context — simulates no fly.toml present.
153+
154+ cfg , copied , err := determineBaseAppConfig (ctx )
155+ require .NoError (t , err )
156+ assert .False (t , copied )
157+ assert .Nil (t , cfg .Build )
158+ })
159+
160+ t .Run ("--copy-config adopts existing config without prompting" , func (t * testing.T ) {
161+ ctx := newDetermineBaseAppConfigCtx (t , true , false )
162+ ctx = appconfig .WithConfig (ctx , existingCfg )
163+
164+ cfg , copied , err := determineBaseAppConfig (ctx )
165+ require .NoError (t , err )
166+ assert .True (t , copied )
167+ assert .Equal (t , "docker.ui-server.dockerfile" , cfg .Build .Dockerfile )
168+ })
169+
170+ t .Run ("explicit --config adopts existing config without prompting" , func (t * testing.T ) {
171+ // This is the deployer scenario: --config fly.custom.toml is passed but
172+ // --copy-config is not. The explicit path signals intent, so we must
173+ // not fall through to source scanning with an empty config.
174+ ctx := newDetermineBaseAppConfigCtx (t , false , true )
175+ ctx = appconfig .WithConfig (ctx , existingCfg )
176+
177+ cfg , copied , err := determineBaseAppConfig (ctx )
178+ require .NoError (t , err )
179+ assert .True (t , copied )
180+ assert .Equal (t , "docker.ui-server.dockerfile" , cfg .Build .Dockerfile )
181+ })
182+
183+ t .Run ("no flags in non-interactive mode returns error" , func (t * testing.T ) {
184+ ctx := newDetermineBaseAppConfigCtx (t , false , false )
185+ ctx = appconfig .WithConfig (ctx , existingCfg )
186+ // Non-interactive iostreams → prompt.Confirm returns ErrNonInteractive.
187+ ios , _ , _ , _ := iostreams .Test ()
188+ ctx = iostreams .NewContext (ctx , ios )
189+
190+ _ , _ , err := determineBaseAppConfig (ctx )
191+ assert .Error (t , err )
192+ })
193+ }
0 commit comments