diff --git a/internal/command/command.go b/internal/command/command.go index de7dc128f6..ef094fa3c2 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -707,7 +707,8 @@ func LoadAppConfigIfPresent(ctx context.Context) (context.Context, error) { } logger := logger.FromContext(ctx) - for _, path := range appConfigFilePaths(ctx) { + configPaths := appConfigFilePaths(ctx) + for _, path := range configPaths { switch cfg, err := appconfig.LoadConfig(path); { case err == nil: logger.Debugf("app config loaded from %s", path) @@ -726,6 +727,11 @@ func LoadAppConfigIfPresent(ctx context.Context) (context.Context, error) { } } + // If the user explicitly specified a config file path and we couldn't find it, return an error + if configPath := flag.GetAppConfigFilePath(ctx); configPath != "" { + return nil, fmt.Errorf("config file not found at specified path: %s (also tried: %s)", configPath, filepath.Join(configPath, appconfig.DefaultConfigFileName)) + } + return ctx, nil } diff --git a/internal/command/load_app_config_test.go b/internal/command/load_app_config_test.go new file mode 100644 index 0000000000..d9433d6eba --- /dev/null +++ b/internal/command/load_app_config_test.go @@ -0,0 +1,80 @@ +package command + +import ( + "context" + "io" + "os" + "path/filepath" + "testing" + + "github.com/spf13/pflag" + "github.com/superfly/flyctl/internal/appconfig" + "github.com/superfly/flyctl/internal/flag" + "github.com/superfly/flyctl/internal/flag/flagnames" + "github.com/superfly/flyctl/internal/logger" + "github.com/superfly/flyctl/internal/state" +) + +func TestLoadAppConfigIfPresent(t *testing.T) { + t.Run("missing explicitly specified config returns an error", func(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "missing.toml") + ctx := loadAppConfigTestContext(t, configPath) + + loadedCtx, err := LoadAppConfigIfPresent(ctx) + if err == nil { + t.Fatal("expected an error for a missing explicitly specified config") + } + if loadedCtx != nil { + t.Fatal("expected a nil context when loading fails") + } + + want := "config file not found at specified path: " + configPath + " (also tried: " + filepath.Join(configPath, appconfig.DefaultConfigFileName) + ")" + if got := err.Error(); got != want { + t.Fatalf("error = %q, want %q", got, want) + } + }) + + t.Run("missing default config is allowed", func(t *testing.T) { + ctx := loadAppConfigTestContext(t, "") + + loadedCtx, err := LoadAppConfigIfPresent(ctx) + if err != nil { + t.Fatalf("LoadAppConfigIfPresent() error = %v", err) + } + if loadedCtx != ctx { + t.Fatal("expected the original context when no default config exists") + } + }) + + t.Run("existing explicitly specified config is loaded", func(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "custom.toml") + if err := os.WriteFile(configPath, []byte("app = \"test-app\"\n"), 0o600); err != nil { + t.Fatal(err) + } + ctx := loadAppConfigTestContext(t, configPath) + + loadedCtx, err := LoadAppConfigIfPresent(ctx) + if err != nil { + t.Fatalf("LoadAppConfigIfPresent() error = %v", err) + } + if cfg := appconfig.ConfigFromContext(loadedCtx); cfg == nil { + t.Fatal("expected the explicit config to be added to the context") + } + }) +} + +func loadAppConfigTestContext(t *testing.T, configPath string) context.Context { + t.Helper() + + fs := pflag.NewFlagSet("test", pflag.ContinueOnError) + fs.String(flagnames.AppConfigFilePath, "", "") + if configPath != "" { + if err := fs.Set(flagnames.AppConfigFilePath, configPath); err != nil { + t.Fatal(err) + } + } + + ctx := flag.NewContext(context.Background(), fs) + ctx = logger.NewContext(ctx, logger.New(io.Discard, logger.NoLogLevel, false)) + return state.WithWorkingDirectory(ctx, t.TempDir()) +}