package build import ( "os" "path/filepath" "testing" "github.com/host-uk/core/pkg/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // setupConfigTestDir creates a temp directory with optional .core/build.yaml content. func setupConfigTestDir(t *testing.T, configContent string) string { t.Helper() dir := t.TempDir() if configContent != "" { coreDir := filepath.Join(dir, ConfigDir) err := os.MkdirAll(coreDir, 0755) require.NoError(t, err) configPath := filepath.Join(coreDir, ConfigFileName) err = os.WriteFile(configPath, []byte(configContent), 0644) require.NoError(t, err) } return dir } func TestLoadConfig_Good(t *testing.T) { fs := io.Local t.Run("loads valid config", func(t *testing.T) { content := ` version: 1 project: name: myapp description: A test application main: ./cmd/myapp binary: myapp build: cgo: true flags: - -trimpath - -race ldflags: - -s - -w env: - FOO=bar targets: - os: linux arch: amd64 - os: darwin arch: arm64 ` dir := setupConfigTestDir(t, content) cfg, err := LoadConfig(fs, dir) require.NoError(t, err) require.NotNil(t, cfg) assert.Equal(t, 1, cfg.Version) assert.Equal(t, "myapp", cfg.Project.Name) assert.Equal(t, "A test application", cfg.Project.Description) assert.Equal(t, "./cmd/myapp", cfg.Project.Main) assert.Equal(t, "myapp", cfg.Project.Binary) assert.True(t, cfg.Build.CGO) assert.Equal(t, []string{"-trimpath", "-race"}, cfg.Build.Flags) assert.Equal(t, []string{"-s", "-w"}, cfg.Build.LDFlags) assert.Equal(t, []string{"FOO=bar"}, cfg.Build.Env) assert.Len(t, cfg.Targets, 2) assert.Equal(t, "linux", cfg.Targets[0].OS) assert.Equal(t, "amd64", cfg.Targets[0].Arch) assert.Equal(t, "darwin", cfg.Targets[1].OS) assert.Equal(t, "arm64", cfg.Targets[1].Arch) }) t.Run("returns defaults when config file missing", func(t *testing.T) { dir := t.TempDir() cfg, err := LoadConfig(fs, dir) require.NoError(t, err) require.NotNil(t, cfg) defaults := DefaultConfig() assert.Equal(t, defaults.Version, cfg.Version) assert.Equal(t, defaults.Project.Main, cfg.Project.Main) assert.Equal(t, defaults.Build.CGO, cfg.Build.CGO) assert.Equal(t, defaults.Build.Flags, cfg.Build.Flags) assert.Equal(t, defaults.Build.LDFlags, cfg.Build.LDFlags) assert.Equal(t, defaults.Targets, cfg.Targets) }) t.Run("applies defaults for missing fields", func(t *testing.T) { content := ` version: 2 project: name: partial ` dir := setupConfigTestDir(t, content) cfg, err := LoadConfig(fs, dir) require.NoError(t, err) require.NotNil(t, cfg) // Explicit values preserved assert.Equal(t, 2, cfg.Version) assert.Equal(t, "partial", cfg.Project.Name) // Defaults applied defaults := DefaultConfig() assert.Equal(t, defaults.Project.Main, cfg.Project.Main) assert.Equal(t, defaults.Build.Flags, cfg.Build.Flags) assert.Equal(t, defaults.Build.LDFlags, cfg.Build.LDFlags) assert.Equal(t, defaults.Targets, cfg.Targets) }) t.Run("preserves empty arrays when explicitly set", func(t *testing.T) { content := ` version: 1 project: name: noflags build: flags: [] ldflags: [] targets: - os: linux arch: amd64 ` dir := setupConfigTestDir(t, content) cfg, err := LoadConfig(fs, dir) require.NoError(t, err) require.NotNil(t, cfg) // Empty arrays are preserved (not replaced with defaults) assert.Empty(t, cfg.Build.Flags) assert.Empty(t, cfg.Build.LDFlags) // Targets explicitly set assert.Len(t, cfg.Targets, 1) }) } func TestLoadConfig_Bad(t *testing.T) { fs := io.Local t.Run("returns error for invalid YAML", func(t *testing.T) { content := ` version: 1 project: name: [invalid yaml ` dir := setupConfigTestDir(t, content) cfg, err := LoadConfig(fs, dir) assert.Error(t, err) assert.Nil(t, cfg) assert.Contains(t, err.Error(), "failed to parse config file") }) t.Run("returns error for unreadable file", func(t *testing.T) { dir := t.TempDir() coreDir := filepath.Join(dir, ConfigDir) err := os.MkdirAll(coreDir, 0755) require.NoError(t, err) // Create config as a directory instead of file configPath := filepath.Join(coreDir, ConfigFileName) err = os.Mkdir(configPath, 0755) require.NoError(t, err) cfg, err := LoadConfig(fs, dir) assert.Error(t, err) assert.Nil(t, cfg) assert.Contains(t, err.Error(), "failed to read config file") }) } func TestDefaultConfig_Good(t *testing.T) { t.Run("returns sensible defaults", func(t *testing.T) { cfg := DefaultConfig() assert.Equal(t, 1, cfg.Version) assert.Equal(t, ".", cfg.Project.Main) assert.Empty(t, cfg.Project.Name) assert.Empty(t, cfg.Project.Binary) assert.False(t, cfg.Build.CGO) assert.Contains(t, cfg.Build.Flags, "-trimpath") assert.Contains(t, cfg.Build.LDFlags, "-s") assert.Contains(t, cfg.Build.LDFlags, "-w") assert.Empty(t, cfg.Build.Env) // Default targets cover common platforms assert.Len(t, cfg.Targets, 4) hasLinuxAmd64 := false hasDarwinArm64 := false hasWindowsAmd64 := false for _, t := range cfg.Targets { if t.OS == "linux" && t.Arch == "amd64" { hasLinuxAmd64 = true } if t.OS == "darwin" && t.Arch == "arm64" { hasDarwinArm64 = true } if t.OS == "windows" && t.Arch == "amd64" { hasWindowsAmd64 = true } } assert.True(t, hasLinuxAmd64) assert.True(t, hasDarwinArm64) assert.True(t, hasWindowsAmd64) }) } func TestConfigPath_Good(t *testing.T) { t.Run("returns correct path", func(t *testing.T) { path := ConfigPath("/project/root") assert.Equal(t, "/project/root/.core/build.yaml", path) }) } func TestConfigExists_Good(t *testing.T) { fs := io.Local t.Run("returns true when config exists", func(t *testing.T) { dir := setupConfigTestDir(t, "version: 1") assert.True(t, ConfigExists(fs, dir)) }) t.Run("returns false when config missing", func(t *testing.T) { dir := t.TempDir() assert.False(t, ConfigExists(fs, dir)) }) t.Run("returns false when .core dir missing", func(t *testing.T) { dir := t.TempDir() assert.False(t, ConfigExists(fs, dir)) }) } func TestLoadConfig_Good_SignConfig(t *testing.T) { tmpDir := t.TempDir() coreDir := filepath.Join(tmpDir, ".core") _ = os.MkdirAll(coreDir, 0755) configContent := `version: 1 sign: enabled: true gpg: key: "ABCD1234" macos: identity: "Developer ID Application: Test" notarize: true ` _ = os.WriteFile(filepath.Join(coreDir, "build.yaml"), []byte(configContent), 0644) cfg, err := LoadConfig(io.Local, tmpDir) if err != nil { t.Fatalf("unexpected error: %v", err) } if !cfg.Sign.Enabled { t.Error("expected Sign.Enabled to be true") } if cfg.Sign.GPG.Key != "ABCD1234" { t.Errorf("expected GPG.Key 'ABCD1234', got %q", cfg.Sign.GPG.Key) } if cfg.Sign.MacOS.Identity != "Developer ID Application: Test" { t.Errorf("expected MacOS.Identity, got %q", cfg.Sign.MacOS.Identity) } if !cfg.Sign.MacOS.Notarize { t.Error("expected MacOS.Notarize to be true") } } func TestBuildConfig_ToTargets_Good(t *testing.T) { t.Run("converts TargetConfig to Target", func(t *testing.T) { cfg := &BuildConfig{ Targets: []TargetConfig{ {OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}, {OS: "windows", Arch: "386"}, }, } targets := cfg.ToTargets() require.Len(t, targets, 3) assert.Equal(t, Target{OS: "linux", Arch: "amd64"}, targets[0]) assert.Equal(t, Target{OS: "darwin", Arch: "arm64"}, targets[1]) assert.Equal(t, Target{OS: "windows", Arch: "386"}, targets[2]) }) t.Run("returns empty slice for no targets", func(t *testing.T) { cfg := &BuildConfig{ Targets: []TargetConfig{}, } targets := cfg.ToTargets() assert.Empty(t, targets) }) } // TestLoadConfig_Testdata tests loading from the testdata fixture. func TestLoadConfig_Testdata(t *testing.T) { fs := io.Local abs, err := filepath.Abs("testdata/config-project") require.NoError(t, err) t.Run("loads config-project fixture", func(t *testing.T) { cfg, err := LoadConfig(fs, abs) require.NoError(t, err) require.NotNil(t, cfg) assert.Equal(t, 1, cfg.Version) assert.Equal(t, "example-cli", cfg.Project.Name) assert.Equal(t, "An example CLI application", cfg.Project.Description) assert.Equal(t, "./cmd/example", cfg.Project.Main) assert.Equal(t, "example", cfg.Project.Binary) assert.False(t, cfg.Build.CGO) assert.Equal(t, []string{"-trimpath"}, cfg.Build.Flags) assert.Equal(t, []string{"-s", "-w"}, cfg.Build.LDFlags) assert.Len(t, cfg.Targets, 3) }) }