diff --git a/cmd/tf/tf_test.go b/cmd/tf/tf_test.go new file mode 100644 index 00000000..e5a66c49 --- /dev/null +++ b/cmd/tf/tf_test.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "os" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/hcl/v2/hclparse" + "github.com/stretchr/testify/assert" + + "github.com/tofuutils/tenv/v4/config" + "github.com/tofuutils/tenv/v4/config/cmdconst" + "github.com/tofuutils/tenv/v4/pkg/loghelper" + "github.com/tofuutils/tenv/v4/versionmanager" + "github.com/tofuutils/tenv/v4/versionmanager/builder" + lightproxy "github.com/tofuutils/tenv/v4/versionmanager/proxy/light" +) + +type mockRetriever struct { + installFunc func(ctx context.Context, version string, targetPath string) error + listVersionsFunc func(ctx context.Context) ([]string, error) +} + +func (m *mockRetriever) Install(ctx context.Context, version string, targetPath string) error { + return m.installFunc(ctx, version, targetPath) +} + +func (m *mockRetriever) ListVersions(ctx context.Context) ([]string, error) { + return m.listVersionsFunc(ctx) +} + +func TestTfCommand(t *testing.T) { + tests := []struct { + name string + args []string + execErr error + expectError bool + }{ + { + name: "successful execution", + args: []string{"version"}, + execErr: nil, + expectError: false, + }, + { + name: "execution error", + args: []string{"invalid"}, + execErr: assert.AnError, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test configuration + logger := hclog.New(&hclog.LoggerOptions{ + Output: os.Stderr, + Level: hclog.Info, + }) + displayer := loghelper.MakeBasicDisplayer(logger, loghelper.StdDisplay) + conf := &config.Config{ + Displayer: displayer, + } + + // Create mock retriever + retriever := &mockRetriever{ + installFunc: func(ctx context.Context, version string, targetPath string) error { + return nil + }, + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return []string{"1.0.0", "1.1.0"}, nil + }, + } + + // Create version manager + vm := versionmanager.Make(conf, "TEST_", "test", nil, retriever, nil) + + // Create a mock builder function + builderFunc := func(conf *config.Config, parser *hclparse.Parser) versionmanager.VersionManager { + return vm + } + + // Save original builder + originalBuilder := builder.Builders[cmdconst.TfName] + builder.Builders[cmdconst.TfName] = builderFunc + defer func() { + builder.Builders[cmdconst.TfName] = originalBuilder + }() + + // Execute command + lightproxy.Exec(cmdconst.TfName) + }) + } +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 00000000..13335ec4 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,208 @@ +package config + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestDefaultConfig(t *testing.T) { + conf, err := DefaultConfig() + if err != nil { + t.Fatalf("DefaultConfig() error = %v", err) + } + + // Verify default values + if conf.Arch != runtime.GOARCH { + t.Errorf("DefaultConfig().Arch = %v, want %v", conf.Arch, runtime.GOARCH) + } + if conf.SkipInstall != true { + t.Errorf("DefaultConfig().SkipInstall = %v, want true", conf.SkipInstall) + } + if conf.remoteConfLoaded != true { + t.Errorf("DefaultConfig().remoteConfLoaded = %v, want true", conf.remoteConfLoaded) + } + if conf.WorkPath != "." { + t.Errorf("DefaultConfig().WorkPath = %v, want .", conf.WorkPath) + } + + // Verify paths + userPath, err := os.UserHomeDir() + if err != nil { + t.Fatalf("os.UserHomeDir() error = %v", err) + } + if conf.UserPath != userPath { + t.Errorf("DefaultConfig().UserPath = %v, want %v", conf.UserPath, userPath) + } + if conf.RootPath != filepath.Join(userPath, defaultDirName) { + t.Errorf("DefaultConfig().RootPath = %v, want %v", conf.RootPath, filepath.Join(userPath, defaultDirName)) + } +} + +func TestInitConfigFromEnv(t *testing.T) { + // Set up test environment variables + os.Setenv("TENV_ARCH", "test-arch") + os.Setenv("TENV_AUTO_INSTALL", "true") + os.Setenv("TENV_FORCE_QUIET", "true") + os.Setenv("TENV_FORCE_REMOTE", "true") + os.Setenv("TENV_GITHUB_ACTIONS", "true") + os.Setenv("TENV_GITHUB_TOKEN", "test-token") + os.Setenv("TENV_REMOTE_CONF_PATH", "/test/path") + os.Setenv("TENV_ROOT_PATH", "/test/root") + os.Setenv("TENV_SKIP_INSTALL", "true") + os.Setenv("TENV_SKIP_SIGNATURE", "true") + os.Setenv("TENV_USER_PATH", "/test/user") + os.Setenv("TENV_WORK_PATH", "/test/work") + + conf, err := InitConfigFromEnv() + if err != nil { + t.Fatalf("InitConfigFromEnv() error = %v", err) + } + + // Verify environment variable values + if conf.Arch != "test-arch" { + t.Errorf("InitConfigFromEnv().Arch = %v, want test-arch", conf.Arch) + } + if conf.SkipInstall != true { + t.Errorf("InitConfigFromEnv().SkipInstall = %v, want true", conf.SkipInstall) + } + if conf.ForceQuiet != true { + t.Errorf("InitConfigFromEnv().ForceQuiet = %v, want true", conf.ForceQuiet) + } + if conf.ForceRemote != true { + t.Errorf("InitConfigFromEnv().ForceRemote = %v, want true", conf.ForceRemote) + } + if conf.GithubActions != true { + t.Errorf("InitConfigFromEnv().GithubActions = %v, want true", conf.GithubActions) + } + if conf.GithubToken != "test-token" { + t.Errorf("InitConfigFromEnv().GithubToken = %v, want test-token", conf.GithubToken) + } + if conf.RemoteConfPath != "/test/path" { + t.Errorf("InitConfigFromEnv().RemoteConfPath = %v, want /test/path", conf.RemoteConfPath) + } + if conf.RootPath != "/test/root" { + t.Errorf("InitConfigFromEnv().RootPath = %v, want /test/root", conf.RootPath) + } + if conf.SkipSignature != true { + t.Errorf("InitConfigFromEnv().SkipSignature = %v, want true", conf.SkipSignature) + } + if conf.UserPath != "/test/user" { + t.Errorf("InitConfigFromEnv().UserPath = %v, want /test/user", conf.UserPath) + } + if conf.WorkPath != "/test/work" { + t.Errorf("InitConfigFromEnv().WorkPath = %v, want /test/work", conf.WorkPath) + } + + // Clean up environment variables + os.Unsetenv("TENV_ARCH") + os.Unsetenv("TENV_AUTO_INSTALL") + os.Unsetenv("TENV_FORCE_QUIET") + os.Unsetenv("TENV_FORCE_REMOTE") + os.Unsetenv("TENV_GITHUB_ACTIONS") + os.Unsetenv("TENV_GITHUB_TOKEN") + os.Unsetenv("TENV_REMOTE_CONF_PATH") + os.Unsetenv("TENV_ROOT_PATH") + os.Unsetenv("TENV_SKIP_INSTALL") + os.Unsetenv("TENV_SKIP_SIGNATURE") + os.Unsetenv("TENV_USER_PATH") + os.Unsetenv("TENV_WORK_PATH") +} + +func TestInitDisplayer(t *testing.T) { + tests := []struct { + name string + proxyCall bool + forceQuiet bool + verbose bool + }{ + { + name: "proxy call with verbose", + proxyCall: true, + forceQuiet: false, + verbose: true, + }, + { + name: "proxy call with quiet", + proxyCall: true, + forceQuiet: true, + verbose: false, + }, + { + name: "direct call with verbose", + proxyCall: false, + forceQuiet: false, + verbose: true, + }, + { + name: "direct call with quiet", + proxyCall: false, + forceQuiet: true, + verbose: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := Config{ + ForceQuiet: tt.forceQuiet, + DisplayVerbose: tt.verbose, + } + conf.InitDisplayer(tt.proxyCall) + if conf.Displayer == nil { + t.Error("InitDisplayer() did not initialize Displayer") + } + }) + } +} + +func TestInitInstall(t *testing.T) { + tests := []struct { + name string + forceInstall bool + forceNoInstall bool + initialSkip bool + expectedSkip bool + }{ + { + name: "force install", + forceInstall: true, + forceNoInstall: false, + initialSkip: true, + expectedSkip: false, + }, + { + name: "force no install", + forceInstall: false, + forceNoInstall: true, + initialSkip: false, + expectedSkip: true, + }, + { + name: "no force", + forceInstall: false, + forceNoInstall: false, + initialSkip: true, + expectedSkip: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := Config{ + SkipInstall: tt.initialSkip, + } + conf.InitInstall(tt.forceInstall, tt.forceNoInstall) + if conf.SkipInstall != tt.expectedSkip { + t.Errorf("InitInstall() SkipInstall = %v, want %v", conf.SkipInstall, tt.expectedSkip) + } + }) + } +} + +func TestEmptyGetenv(t *testing.T) { + if got := EmptyGetenv("TEST_KEY"); got != "" { + t.Errorf("EmptyGetenv() = %v, want empty string", got) + } +} diff --git a/config/remote_test.go b/config/remote_test.go new file mode 100644 index 00000000..7d4ee54c --- /dev/null +++ b/config/remote_test.go @@ -0,0 +1,216 @@ +package config + +import ( + "testing" + + configutils "github.com/tofuutils/tenv/v4/config/utils" + "github.com/tofuutils/tenv/v4/pkg/download" +) + +func TestMakeDefaultRemoteConfig(t *testing.T) { + defaultURL := "https://example.com" + defaultBaseURL := "https://base.example.com" + + config := makeDefaultRemoteConfig(defaultURL, defaultBaseURL) + + if config.defaultURL != defaultURL { + t.Errorf("makeDefaultRemoteConfig().defaultURL = %v, want %v", config.defaultURL, defaultURL) + } + if config.defaultBaseURL != defaultBaseURL { + t.Errorf("makeDefaultRemoteConfig().defaultBaseURL = %v, want %v", config.defaultBaseURL, defaultBaseURL) + } +} + +func TestMakeRemoteConfig(t *testing.T) { + tests := []struct { + name string + getenv configutils.GetenvFunc + remoteURLEnv string + listURLEnv string + installModeEnv string + listModeEnv string + defaultURL string + defaultBaseURL string + wantRemoteURL string + wantListURL string + wantInstallMode string + wantListMode string + }{ + { + name: "all defaults", + getenv: configutils.EmptyGetenv, + remoteURLEnv: "TEST_REMOTE_URL", + listURLEnv: "TEST_LIST_URL", + installModeEnv: "TEST_INSTALL_MODE", + listModeEnv: "TEST_LIST_MODE", + defaultURL: "https://example.com", + defaultBaseURL: "https://base.example.com", + wantRemoteURL: "https://example.com", + wantListURL: "https://example.com", + wantInstallMode: InstallModeDirect, + wantListMode: ListModeHTML, + }, + { + name: "all custom", + getenv: func(key string) string { + switch key { + case "TEST_REMOTE_URL": + return "https://custom.example.com" + case "TEST_LIST_URL": + return "https://list.example.com" + case "TEST_INSTALL_MODE": + return "custom" + case "TEST_LIST_MODE": + return "custom" + } + return "" + }, + remoteURLEnv: "TEST_REMOTE_URL", + listURLEnv: "TEST_LIST_URL", + installModeEnv: "TEST_INSTALL_MODE", + listModeEnv: "TEST_LIST_MODE", + defaultURL: "https://example.com", + defaultBaseURL: "https://base.example.com", + wantRemoteURL: "https://custom.example.com", + wantListURL: "https://list.example.com", + wantInstallMode: "custom", + wantListMode: "custom", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := makeRemoteConfig(tt.getenv, tt.remoteURLEnv, tt.listURLEnv, tt.installModeEnv, tt.listModeEnv, tt.defaultURL, tt.defaultBaseURL) + + if config.GetRemoteURL() != tt.wantRemoteURL { + t.Errorf("makeRemoteConfig().GetRemoteURL() = %v, want %v", config.GetRemoteURL(), tt.wantRemoteURL) + } + if config.GetListURL() != tt.wantListURL { + t.Errorf("makeRemoteConfig().GetListURL() = %v, want %v", config.GetListURL(), tt.wantListURL) + } + if config.GetInstallMode() != tt.wantInstallMode { + t.Errorf("makeRemoteConfig().GetInstallMode() = %v, want %v", config.GetInstallMode(), tt.wantInstallMode) + } + if config.GetListMode() != tt.wantListMode { + t.Errorf("makeRemoteConfig().GetListMode() = %v, want %v", config.GetListMode(), tt.wantListMode) + } + }) + } +} + +func TestRemoteConfig_GetRewriteRule(t *testing.T) { + tests := []struct { + name string + config RemoteConfig + wantRule download.URLTransformer + }{ + { + name: "no rewrite rule", + config: RemoteConfig{ + Data: map[string]string{}, + }, + wantRule: nil, + }, + { + name: "with rewrite rule", + config: RemoteConfig{ + Data: map[string]string{ + "rewrite_rule": "s/old/new/", + }, + }, + wantRule: download.URLTransformer(func(url string) string { + return "new" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rule := tt.config.GetRewriteRule() + if tt.wantRule == nil && rule != nil { + t.Error("GetRewriteRule() returned non-nil rule when expected nil") + } + if tt.wantRule != nil && rule == nil { + t.Error("GetRewriteRule() returned nil rule when expected non-nil") + } + }) + } +} + +func TestMapGetDefault(t *testing.T) { + tests := []struct { + name string + m map[string]string + key string + defaultValue string + want string + }{ + { + name: "key exists", + m: map[string]string{"test": "value"}, + key: "test", + defaultValue: "default", + want: "value", + }, + { + name: "key does not exist", + m: map[string]string{}, + key: "test", + defaultValue: "default", + want: "default", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MapGetDefault(tt.m, tt.key, tt.defaultValue); got != tt.want { + t.Errorf("MapGetDefault() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetBasicAuthOption(t *testing.T) { + tests := []struct { + name string + getenv configutils.GetenvFunc + userEnv string + passEnv string + wantOption bool + }{ + { + name: "no auth", + getenv: configutils.EmptyGetenv, + userEnv: "TEST_USER", + passEnv: "TEST_PASS", + wantOption: false, + }, + { + name: "with auth", + getenv: func(key string) string { + switch key { + case "TEST_USER": + return "user" + case "TEST_PASS": + return "pass" + } + return "" + }, + userEnv: "TEST_USER", + passEnv: "TEST_PASS", + wantOption: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := GetBasicAuthOption(tt.getenv, tt.userEnv, tt.passEnv) + if tt.wantOption && len(options) == 0 { + t.Error("GetBasicAuthOption() returned no options when expected some") + } + if !tt.wantOption && len(options) > 0 { + t.Error("GetBasicAuthOption() returned options when expected none") + } + }) + } +} diff --git a/config/utils/utils_test.go b/config/utils/utils_test.go new file mode 100644 index 00000000..e0da1af3 --- /dev/null +++ b/config/utils/utils_test.go @@ -0,0 +1,210 @@ +package configutils + +import ( + "testing" +) + +func TestGetenvFunc_Bool(t *testing.T) { + tests := []struct { + name string + getenv GetenvFunc + defaultValue bool + key string + want bool + wantErr bool + }{ + { + name: "true value", + getenv: func(string) string { return "true" }, + defaultValue: false, + key: "TEST_KEY", + want: true, + wantErr: false, + }, + { + name: "false value", + getenv: func(string) string { return "false" }, + defaultValue: true, + key: "TEST_KEY", + want: false, + wantErr: false, + }, + { + name: "empty value", + getenv: func(string) string { return "" }, + defaultValue: true, + key: "TEST_KEY", + want: true, + wantErr: false, + }, + { + name: "invalid value", + getenv: func(string) string { return "invalid" }, + defaultValue: false, + key: "TEST_KEY", + want: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.getenv.Bool(tt.defaultValue, tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("GetenvFunc.Bool() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetenvFunc.Bool() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetenvFunc_BoolFallback(t *testing.T) { + tests := []struct { + name string + getenv GetenvFunc + defaultValue bool + keys []string + want bool + wantErr bool + }{ + { + name: "first key has value", + getenv: func(key string) string { + if key == "KEY1" { + return "true" + } + return "" + }, + defaultValue: false, + keys: []string{"KEY1", "KEY2"}, + want: true, + wantErr: false, + }, + { + name: "second key has value", + getenv: func(key string) string { + if key == "KEY2" { + return "true" + } + return "" + }, + defaultValue: false, + keys: []string{"KEY1", "KEY2"}, + want: true, + wantErr: false, + }, + { + name: "no keys have value", + getenv: func(string) string { return "" }, + defaultValue: true, + keys: []string{"KEY1", "KEY2"}, + want: true, + wantErr: false, + }, + { + name: "invalid value", + getenv: func(key string) string { + if key == "KEY1" { + return "invalid" + } + return "" + }, + defaultValue: false, + keys: []string{"KEY1", "KEY2"}, + want: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.getenv.BoolFallback(tt.defaultValue, tt.keys...) + if (err != nil) != tt.wantErr { + t.Errorf("GetenvFunc.BoolFallback() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetenvFunc.BoolFallback() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetenvFunc_Fallback(t *testing.T) { + tests := []struct { + name string + getenv GetenvFunc + keys []string + want string + }{ + { + name: "first key has value", + getenv: func(key string) string { + if key == "KEY1" { + return "value1" + } + return "" + }, + keys: []string{"KEY1", "KEY2"}, + want: "value1", + }, + { + name: "second key has value", + getenv: func(key string) string { + if key == "KEY2" { + return "value2" + } + return "" + }, + keys: []string{"KEY1", "KEY2"}, + want: "value2", + }, + { + name: "no keys have value", + getenv: func(string) string { return "" }, + keys: []string{"KEY1", "KEY2"}, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.getenv.Fallback(tt.keys...); got != tt.want { + t.Errorf("GetenvFunc.Fallback() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetenvFunc_Present(t *testing.T) { + tests := []struct { + name string + getenv GetenvFunc + key string + want bool + }{ + { + name: "key present", + getenv: func(string) string { return "value" }, + key: "TEST_KEY", + want: true, + }, + { + name: "key not present", + getenv: func(string) string { return "" }, + key: "TEST_KEY", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.getenv.Present(tt.key); got != tt.want { + t.Errorf("GetenvFunc.Present() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/coverage.txt b/coverage.txt new file mode 100644 index 00000000..5f02b111 --- /dev/null +++ b/coverage.txt @@ -0,0 +1 @@ +mode: set diff --git a/pkg/check/cosign/check_test.go b/pkg/check/cosign/check_test.go index 93b5252c..fa95cff3 100644 --- a/pkg/check/cosign/check_test.go +++ b/pkg/check/cosign/check_test.go @@ -16,65 +16,140 @@ * */ -package cosigncheck_test +package cosigncheck import ( - _ "embed" + "os" "testing" - cosigncheck "github.com/tofuutils/tenv/v4/pkg/check/cosign" - "github.com/tofuutils/tenv/v4/pkg/loghelper" -) + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/assert" -const ( - identity = "https://github.com/opentofu/opentofu/.github/workflows/release.yml@refs/heads/v1.6" - issuer = "https://token.actions.githubusercontent.com" + "github.com/tofuutils/tenv/v4/pkg/loghelper" ) -//go:embed testdata/tofu_1.6.0_SHA256SUMS -var data []byte - -//go:embed testdata/tofu_1.6.0_SHA256SUMS.sig -var dataSig []byte +func TestCheck(t *testing.T) { + tests := []struct { + name string + data []byte + dataSig []byte + dataCert []byte + certIdentity string + certOidcIssuer string + expectError bool + }{ + { + name: "valid signature", + data: []byte("test content"), + dataSig: []byte("test signature"), + dataCert: []byte("test certificate"), + certIdentity: "test@example.com", + certOidcIssuer: "https://accounts.example.com", + expectError: false, + }, + { + name: "invalid signature", + data: []byte("test content"), + dataSig: []byte("invalid signature"), + dataCert: []byte("test certificate"), + certIdentity: "test@example.com", + certOidcIssuer: "https://accounts.example.com", + expectError: true, + }, + { + name: "empty certificate", + data: []byte("test content"), + dataSig: []byte("test signature"), + dataCert: []byte(""), + certIdentity: "test@example.com", + certOidcIssuer: "https://accounts.example.com", + expectError: true, + }, + { + name: "empty signature", + data: []byte("test content"), + dataSig: []byte(""), + dataCert: []byte("test certificate"), + certIdentity: "test@example.com", + certOidcIssuer: "https://accounts.example.com", + expectError: true, + }, + { + name: "empty data", + data: []byte(""), + dataSig: []byte("test signature"), + dataCert: []byte("test certificate"), + certIdentity: "test@example.com", + certOidcIssuer: "https://accounts.example.com", + expectError: true, + }, + { + name: "invalid certificate format", + data: []byte("test content"), + dataSig: []byte("test signature"), + dataCert: []byte("invalid certificate format"), + certIdentity: "test@example.com", + certOidcIssuer: "https://accounts.example.com", + expectError: true, + }, + { + name: "mismatched identity", + data: []byte("test content"), + dataSig: []byte("test signature"), + dataCert: []byte("test certificate"), + certIdentity: "wrong@example.com", + certOidcIssuer: "https://accounts.example.com", + expectError: true, + }, + { + name: "mismatched issuer", + data: []byte("test content"), + dataSig: []byte("test signature"), + dataCert: []byte("test certificate"), + certIdentity: "test@example.com", + certOidcIssuer: "https://wrong.example.com", + expectError: true, + }, + } -//go:embed testdata/tofu_1.6.0_SHA256SUMS.pem -var dataCert []byte + // Create test configuration + logger := hclog.New(&hclog.LoggerOptions{ + Output: os.Stderr, + Level: hclog.Info, + }) + displayer := loghelper.MakeBasicDisplayer(logger, loghelper.StdDisplay) -/* - * no "t.Parallel()" on those tests (causes failures in cosign call) - */ + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Run check + err := Check(tt.data, tt.dataSig, tt.dataCert, tt.certIdentity, tt.certOidcIssuer, displayer) -func TestCosignCheckCorrect(t *testing.T) { //nolint - t.SkipNow() - if err := cosigncheck.Check(data, dataSig, dataCert, identity, issuer, loghelper.InertDisplayer); err != nil { - t.Error("Unexpected error :", err) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) } } -func TestCosignCheckErrorCert(t *testing.T) { //nolint - t.SkipNow() - if cosigncheck.Check(data, dataSig, dataCert[1:], identity, issuer, loghelper.InertDisplayer) == nil { - t.Error("Should fail on erroneous certificate") - } +func TestCheckWithNilDisplayer(t *testing.T) { + // Test with nil displayer + err := Check([]byte("test"), []byte("sig"), []byte("cert"), "test@example.com", "https://accounts.example.com", nil) + assert.Error(t, err) } -func TestCosignCheckErrorIdentity(t *testing.T) { //nolint - t.SkipNow() - if cosigncheck.Check(data, dataSig, dataCert, "me", issuer, loghelper.InertDisplayer) == nil { - t.Error("Should fail on erroneous issuer") - } -} +func TestCheckWithInvalidCertificate(t *testing.T) { + // Test with invalid certificate format + logger := hclog.New(&hclog.LoggerOptions{ + Output: os.Stderr, + Level: hclog.Info, + }) + displayer := loghelper.MakeBasicDisplayer(logger, loghelper.StdDisplay) -func TestCosignCheckErrorIssuer(t *testing.T) { //nolint - t.SkipNow() - if cosigncheck.Check(data, dataSig, dataCert, identity, "http://myself.com", loghelper.InertDisplayer) == nil { - t.Error("Should fail on erroneous issuer") - } -} + // Create a certificate with invalid format + invalidCert := []byte("-----BEGIN CERTIFICATE-----\ninvalid\n-----END CERTIFICATE-----") -func TestCosignCheckErrorSig(t *testing.T) { //nolint - t.SkipNow() - if cosigncheck.Check(data, dataSig[1:], dataCert, identity, issuer, loghelper.InertDisplayer) == nil { - t.Error("Should fail on erroneous signature") - } + err := Check([]byte("test"), []byte("sig"), invalidCert, "test@example.com", "https://accounts.example.com", displayer) + assert.Error(t, err) } diff --git a/pkg/cmdproxy/proxy_test.go b/pkg/cmdproxy/proxy_test.go new file mode 100644 index 00000000..f748bace --- /dev/null +++ b/pkg/cmdproxy/proxy_test.go @@ -0,0 +1,244 @@ +/* + * + * Copyright 2024 tofuutils authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package cmdproxy_test + +import ( + "bytes" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/tofuutils/tenv/v4/pkg/cmdproxy" +) + +func TestWriteMultiline(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + key string + value string + wantErr bool + wantWrite string + }{ + { + name: "basic write", + key: "test", + value: "value", + wantErr: false, + wantWrite: "test<html") + _, err := extractList(invalidHTML, "a", selectionTextExtractor) + if err == nil { + t.Error("Expected error for invalid HTML") + } + + // Test with nil extractor + _, err = extractList(artifactoryData, "a", nil) + if err == nil { + t.Error("Expected error for nil extractor") + } +} diff --git a/pkg/iacparser/parser.go b/pkg/iacparser/parser.go new file mode 100644 index 00000000..1718ef16 --- /dev/null +++ b/pkg/iacparser/parser.go @@ -0,0 +1,21 @@ +package iacparser + +// ExtDescription represents a file extension and its description +type ExtDescription struct { + Ext string + Description string +} + +// DefaultExtensions returns the default list of infrastructure-as-code file extensions +func DefaultExtensions() []ExtDescription { + return []ExtDescription{ + {Ext: ".tf", Description: "Terraform configuration file"}, + {Ext: ".tf.json", Description: "Terraform JSON configuration file"}, + {Ext: ".tfvars", Description: "Terraform variables file"}, + {Ext: ".tfvars.json", Description: "Terraform JSON variables file"}, + {Ext: ".hcl", Description: "HCL configuration file"}, + {Ext: ".json", Description: "JSON configuration file"}, + {Ext: ".yaml", Description: "YAML configuration file"}, + {Ext: ".yml", Description: "YAML configuration file"}, + } +} diff --git a/pkg/lockfile/lockfile_test.go b/pkg/lockfile/lockfile_test.go index eba0cada..04c3f6b2 100644 --- a/pkg/lockfile/lockfile_test.go +++ b/pkg/lockfile/lockfile_test.go @@ -106,3 +106,81 @@ func writeReadFile(dirPath string, filePath string, data []byte, displayer loghe return os.ReadFile(filePath) } + +func TestCleanAndExitOnInterrupt(t *testing.T) { + t.Parallel() + + cleanCalled := false + clean := func() { + cleanCalled = true + } + + // Start the interrupt handler + stop := lockfile.CleanAndExitOnInterrupt(clean) + + // Stop the handler without sending interrupt + stop() + + if cleanCalled { + t.Error("Clean function should not have been called") + } +} + +func TestWriteLockFileExists(t *testing.T) { + t.Parallel() + + testDirPath := filepath.Join(os.TempDir(), "locktest") + err := os.MkdirAll(testDirPath, 0o755) + if err != nil { + t.Fatal("Failed to create test directory:", err) + } + defer os.RemoveAll(testDirPath) + + // Create a lock file manually + lockPath := filepath.Join(testDirPath, ".lock") + if err := os.WriteFile(lockPath, []byte("test"), fileperm.RW); err != nil { + t.Fatal("Failed to create test lock file:", err) + } + + // Try to acquire lock with a timeout to avoid infinite wait + done := make(chan bool) + go func() { + deleteLock := lockfile.Write(testDirPath, loghelper.InertDisplayer) + defer deleteLock() + done <- true + }() + + // Wait for a short time to ensure the lock is attempted + select { + case <-done: + // Lock was acquired (after the existing one was considered stale) + case <-time.After(3 * time.Second): + t.Error("Lock acquisition took too long") + } +} + +func TestWriteLockFilePermissions(t *testing.T) { + t.Parallel() + + testDirPath := filepath.Join(os.TempDir(), "locktest-perm") + err := os.MkdirAll(testDirPath, 0o755) + if err != nil { + t.Fatal("Failed to create test directory:", err) + } + defer os.RemoveAll(testDirPath) + + deleteLock := lockfile.Write(testDirPath, loghelper.InertDisplayer) + defer deleteLock() + + // Check if lock file exists with correct permissions + lockPath := filepath.Join(testDirPath, ".lock") + info, err := os.Stat(lockPath) + if err != nil { + t.Fatal("Lock file was not created:", err) + } + + // Check permissions (considering umask) + if info.Mode().Perm()&0o600 != 0o600 { + t.Errorf("Lock file has wrong permissions. Got: %v, want at least: %v", info.Mode().Perm(), 0o600) + } +} diff --git a/pkg/pathfilter/pathfilter_test.go b/pkg/pathfilter/pathfilter_test.go new file mode 100644 index 00000000..3b52fcd2 --- /dev/null +++ b/pkg/pathfilter/pathfilter_test.go @@ -0,0 +1,103 @@ +/* + * + * Copyright 2024 tofuutils authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package pathfilter_test + +import ( + "testing" + + "github.com/tofuutils/tenv/v4/pkg/pathfilter" +) + +func TestNameEqual(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + targetName string + path string + want bool + }{ + { + name: "Unix path match", + targetName: "file.txt", + path: "/path/to/file.txt", + want: true, + }, + { + name: "Unix path no match", + targetName: "file.txt", + path: "/path/to/other.txt", + want: false, + }, + { + name: "Windows path match", + targetName: "file.txt", + path: `C:\path\to\file.txt`, + want: true, + }, + { + name: "Windows path no match", + targetName: "file.txt", + path: `C:\path\to\other.txt`, + want: false, + }, + { + name: "No directory separator match", + targetName: "file.txt", + path: "file.txt", + want: true, + }, + { + name: "No directory separator no match", + targetName: "file.txt", + path: "other.txt", + want: false, + }, + { + name: "Mixed separators match", + targetName: "file.txt", + path: `path/to\file.txt`, + want: true, + }, + { + name: "Empty target name", + targetName: "", + path: "/path/to/", + want: true, + }, + { + name: "Empty path", + targetName: "file.txt", + path: "", + want: false, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + filter := pathfilter.NameEqual(tt.targetName) + got := filter(tt.path) + if got != tt.want { + t.Errorf("NameEqual(%q)(%q) = %v, want %v", tt.targetName, tt.path, got, tt.want) + } + }) + } +} diff --git a/pkg/reversecmp/reverse_test.go b/pkg/reversecmp/reverse_test.go index 8e99b33b..935d892c 100644 --- a/pkg/reversecmp/reverse_test.go +++ b/pkg/reversecmp/reverse_test.go @@ -54,3 +54,392 @@ func TestReverserTrue(t *testing.T) { t.Error("Not inversed again") } } + +func TestReverserString(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + a string + b string + reverseOrder bool + want int + }{ + { + name: "forward empty strings", + a: "", + b: "", + reverseOrder: false, + want: 0, + }, + { + name: "forward lexicographic order", + a: "abc", + b: "def", + reverseOrder: false, + want: -1, + }, + { + name: "reverse lexicographic order", + a: "abc", + b: "def", + reverseOrder: true, + want: 1, + }, + { + name: "forward with unicode", + a: "世界", + b: "你好", + reverseOrder: false, + want: 1, + }, + { + name: "reverse with unicode", + a: "世界", + b: "你好", + reverseOrder: true, + want: -1, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + reversed := reversecmp.Reverser[string](cmp.Compare[string], tt.reverseOrder) + got := reversed(tt.a, tt.b) + if got != tt.want { + t.Errorf("Reverser(%q, %q) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} + +func TestReverserFloat(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + a float64 + b float64 + reverseOrder bool + want int + }{ + { + name: "forward equal floats", + a: 0.0, + b: 0.0, + reverseOrder: false, + want: 0, + }, + { + name: "forward positive floats", + a: 1.5, + b: 2.5, + reverseOrder: false, + want: -1, + }, + { + name: "reverse positive floats", + a: 1.5, + b: 2.5, + reverseOrder: true, + want: 1, + }, + { + name: "forward negative floats", + a: -2.5, + b: -1.5, + reverseOrder: false, + want: -1, + }, + { + name: "reverse negative floats", + a: -2.5, + b: -1.5, + reverseOrder: true, + want: 1, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + reversed := reversecmp.Reverser[float64](cmp.Compare[float64], tt.reverseOrder) + got := reversed(tt.a, tt.b) + if got != tt.want { + t.Errorf("Reverser(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} + +// Custom type to test with custom comparison function +type Version struct { + Major, Minor, Patch int +} + +func compareVersion(a, b Version) int { + if a.Major != b.Major { + return cmp.Compare(a.Major, b.Major) + } + if a.Minor != b.Minor { + return cmp.Compare(a.Minor, b.Minor) + } + return cmp.Compare(a.Patch, b.Patch) +} + +func TestReverserCustomType(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + a Version + b Version + reverseOrder bool + want int + }{ + { + name: "forward equal versions", + a: Version{1, 0, 0}, + b: Version{1, 0, 0}, + reverseOrder: false, + want: 0, + }, + { + name: "forward major version diff", + a: Version{1, 0, 0}, + b: Version{2, 0, 0}, + reverseOrder: false, + want: -1, + }, + { + name: "reverse major version diff", + a: Version{1, 0, 0}, + b: Version{2, 0, 0}, + reverseOrder: true, + want: 1, + }, + { + name: "forward minor version diff", + a: Version{1, 1, 0}, + b: Version{1, 2, 0}, + reverseOrder: false, + want: -1, + }, + { + name: "forward patch version diff", + a: Version{1, 0, 1}, + b: Version{1, 0, 2}, + reverseOrder: false, + want: -1, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + reversed := reversecmp.Reverser[Version](compareVersion, tt.reverseOrder) + got := reversed(tt.a, tt.b) + if got != tt.want { + t.Errorf("Reverser(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} + +func TestReverserInt8(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + a int8 + b int8 + reverseOrder bool + want int + }{ + { + name: "forward equal values", + a: 0, + b: 0, + reverseOrder: false, + want: 0, + }, + { + name: "forward a < b", + a: 1, + b: 2, + reverseOrder: false, + want: -1, + }, + { + name: "reverse a < b", + a: 1, + b: 2, + reverseOrder: true, + want: 1, + }, + { + name: "forward a > b", + a: 2, + b: 1, + reverseOrder: false, + want: 1, + }, + { + name: "reverse a > b", + a: 2, + b: 1, + reverseOrder: true, + want: -1, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + reversed := reversecmp.Reverser[int8](cmp.Compare[int8], tt.reverseOrder) + got := reversed(tt.a, tt.b) + if got != tt.want { + t.Errorf("Reverser(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} + +func TestReverserUint16(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + a uint16 + b uint16 + reverseOrder bool + want int + }{ + { + name: "forward equal values", + a: 0, + b: 0, + reverseOrder: false, + want: 0, + }, + { + name: "forward a < b", + a: 1, + b: 2, + reverseOrder: false, + want: -1, + }, + { + name: "reverse a < b", + a: 1, + b: 2, + reverseOrder: true, + want: 1, + }, + { + name: "forward a > b", + a: 2, + b: 1, + reverseOrder: false, + want: 1, + }, + { + name: "reverse a > b", + a: 2, + b: 1, + reverseOrder: true, + want: -1, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + reversed := reversecmp.Reverser[uint16](cmp.Compare[uint16], tt.reverseOrder) + got := reversed(tt.a, tt.b) + if got != tt.want { + t.Errorf("Reverser(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} + +func TestReverserFloat32(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + a float32 + b float32 + reverseOrder bool + want int + }{ + { + name: "forward equal values", + a: 0.0, + b: 0.0, + reverseOrder: false, + want: 0, + }, + { + name: "forward a < b", + a: 1.5, + b: 2.5, + reverseOrder: false, + want: -1, + }, + { + name: "reverse a < b", + a: 1.5, + b: 2.5, + reverseOrder: true, + want: 1, + }, + { + name: "forward a > b", + a: 2.5, + b: 1.5, + reverseOrder: false, + want: 1, + }, + { + name: "reverse a > b", + a: 2.5, + b: 1.5, + reverseOrder: true, + want: -1, + }, + { + name: "forward negative values", + a: -2.5, + b: -1.5, + reverseOrder: false, + want: -1, + }, + { + name: "reverse negative values", + a: -2.5, + b: -1.5, + reverseOrder: true, + want: 1, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + reversed := reversecmp.Reverser[float32](cmp.Compare[float32], tt.reverseOrder) + got := reversed(tt.a, tt.b) + if got != tt.want { + t.Errorf("Reverser(%v, %v) = %v, want %v", tt.a, tt.b, got, tt.want) + } + }) + } +} diff --git a/pkg/zip/zip_test.go b/pkg/zip/zip_test.go new file mode 100644 index 00000000..89b1a02b --- /dev/null +++ b/pkg/zip/zip_test.go @@ -0,0 +1,455 @@ +/* + * + * Copyright 2024 tofuutils authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package zip_test + +import ( + "archive/zip" + "bytes" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + ziputil "github.com/tofuutils/tenv/v4/pkg/zip" +) + +func createTestZip(files map[string][]byte) ([]byte, error) { + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + for name, content := range files { + f, err := w.Create(name) + if err != nil { + return nil, err + } + if _, err := f.Write(content); err != nil { + return nil, err + } + } + + if err := w.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func TestUnzipToDir(t *testing.T) { + t.Parallel() + + // Create test data + files := map[string][]byte{ + "file1.txt": []byte("content1"), + "dir1/file2.txt": []byte("content2"), + "dir1/dir2/file3.txt": []byte("content3"), + "dir1/": nil, // directory entry + } + + zipData, err := createTestZip(files) + if err != nil { + t.Fatal("Failed to create test zip:", err) + } + + tests := []struct { + name string + zipData []byte + filter func(string) bool + wantErr bool + }{ + { + name: "basic unzip", + zipData: zipData, + filter: func(string) bool { return true }, + wantErr: false, + }, + { + name: "filtered unzip", + zipData: zipData, + filter: func(path string) bool { return filepath.Base(path) == "file1.txt" }, + wantErr: false, + }, + { + name: "invalid zip data", + zipData: []byte("not a zip file"), + filter: func(string) bool { return true }, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt // capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create temp directory for each test + tempDir, err := os.MkdirTemp("", "zip_test_*") + if err != nil { + t.Fatal("Failed to create temp dir:", err) + } + defer os.RemoveAll(tempDir) + + // Test unzip + err = ziputil.UnzipToDir(tt.zipData, tempDir, tt.filter) + if (err != nil) != tt.wantErr { + t.Errorf("UnzipToDir() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr { + return + } + + // Verify results for successful cases + for name, content := range files { + path := filepath.Join(tempDir, name) + if name[len(name)-1] == '/' { + // Check directory exists + if info, err := os.Stat(path); err != nil || !info.IsDir() { + t.Errorf("Directory %s was not created properly", name) + } + continue + } + + if tt.filter(path) { + // Check file exists and content matches + gotContent, err := os.ReadFile(path) + if err != nil { + t.Errorf("Failed to read file %s: %v", name, err) + continue + } + if !bytes.Equal(gotContent, content) { + t.Errorf("File %s content mismatch: got %q, want %q", name, gotContent, content) + } + } else { + // Check file doesn't exist if filtered out + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Errorf("File %s should not exist", name) + } + } + } + }) + } +} + +func TestSanitizeArchivePath(t *testing.T) { + t.Parallel() + + // Create test zip with path traversal attempt + files := map[string][]byte{ + "../../../etc/passwd": []byte("malicious"), + "normal.txt": []byte("safe"), + } + + zipData, err := createTestZip(files) + if err != nil { + t.Fatal("Failed to create test zip:", err) + } + + tempDir, err := os.MkdirTemp("", "zip_security_test_*") + if err != nil { + t.Fatal("Failed to create temp dir:", err) + } + defer os.RemoveAll(tempDir) + + // Test unzip with potential path traversal + err = ziputil.UnzipToDir(zipData, tempDir, func(string) bool { return true }) + if err == nil { + t.Error("Expected error for path traversal attempt") + } + + // Verify no files were created outside temp directory + parentDir := filepath.Dir(tempDir) + entries, err := os.ReadDir(parentDir) + if err != nil { + t.Fatal("Failed to read parent directory:", err) + } + + for _, entry := range entries { + if entry.Name() != filepath.Base(tempDir) { + t.Errorf("Unexpected file created: %s", entry.Name()) + } + } +} + +func TestUnzipWithEmptyFilter(t *testing.T) { + t.Parallel() + + files := map[string][]byte{ + "file1.txt": []byte("content1"), + } + + zipData, err := createTestZip(files) + if err != nil { + t.Fatal("Failed to create test zip:", err) + } + + tempDir, err := os.MkdirTemp("", "zip_empty_filter_*") + if err != nil { + t.Fatal("Failed to create temp dir:", err) + } + defer os.RemoveAll(tempDir) + + // Test with filter that rejects everything + err = ziputil.UnzipToDir(zipData, tempDir, func(string) bool { return false }) + if err != nil { + t.Error("Unexpected error with empty filter:", err) + } + + // Verify no files were extracted + entries, err := os.ReadDir(tempDir) + if err != nil { + t.Fatal("Failed to read directory:", err) + } + + if len(entries) > 0 { + t.Error("Expected no files to be extracted") + } +} + +func TestUnzipWithFilePermissions(t *testing.T) { + t.Parallel() + + // Create a zip with files having different permissions + files := map[string][]byte{ + "executable.sh": []byte("#!/bin/sh\necho hello"), + "readonly.txt": []byte("read only content"), + } + + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + // Create executable file + execHeader := &zip.FileHeader{ + Name: "executable.sh", + Method: zip.Deflate, + } + execHeader.SetMode(0755) + f1, err := w.CreateHeader(execHeader) + if err != nil { + t.Fatal(err) + } + if _, err := f1.Write(files["executable.sh"]); err != nil { + t.Fatal(err) + } + + // Create read-only file + readHeader := &zip.FileHeader{ + Name: "readonly.txt", + Method: zip.Deflate, + } + readHeader.SetMode(0444) + f2, err := w.CreateHeader(readHeader) + if err != nil { + t.Fatal(err) + } + if _, err := f2.Write(files["readonly.txt"]); err != nil { + t.Fatal(err) + } + + if err := w.Close(); err != nil { + t.Fatal(err) + } + + tempDir, err := os.MkdirTemp("", "zip_perms_test_*") + if err != nil { + t.Fatal("Failed to create temp dir:", err) + } + defer os.RemoveAll(tempDir) + + // Test unzip + err = ziputil.UnzipToDir(buf.Bytes(), tempDir, func(string) bool { return true }) + if err != nil { + t.Fatal("Failed to unzip:", err) + } + + // Verify file permissions + execPath := filepath.Join(tempDir, "executable.sh") + info, err := os.Stat(execPath) + if err != nil { + t.Fatal("Failed to stat executable file:", err) + } + if info.Mode().Perm() != 0755 { + t.Errorf("Executable file has wrong permissions: got %v, want %v", info.Mode().Perm(), 0755) + } + + readPath := filepath.Join(tempDir, "readonly.txt") + info, err = os.Stat(readPath) + if err != nil { + t.Fatal("Failed to stat readonly file:", err) + } + if info.Mode().Perm() != 0444 { + t.Errorf("Read-only file has wrong permissions: got %v, want %v", info.Mode().Perm(), 0444) + } +} + +func TestUnzipWithEmptyDirectories(t *testing.T) { + t.Parallel() + + buf := new(bytes.Buffer) + w := zip.NewWriter(buf) + + // Create empty directories + dirs := []string{ + "empty1/", + "empty2/", + "nested/empty3/", + "nested/empty4/", + } + + for _, dir := range dirs { + _, err := w.Create(dir) + if err != nil { + t.Fatal(err) + } + } + + if err := w.Close(); err != nil { + t.Fatal(err) + } + + tempDir, err := os.MkdirTemp("", "zip_empty_dirs_*") + if err != nil { + t.Fatal("Failed to create temp dir:", err) + } + defer os.RemoveAll(tempDir) + + // Test unzip + err = ziputil.UnzipToDir(buf.Bytes(), tempDir, func(string) bool { return true }) + if err != nil { + t.Fatal("Failed to unzip:", err) + } + + // Verify directories were created + for _, dir := range dirs { + path := filepath.Join(tempDir, dir) + info, err := os.Stat(path) + if err != nil { + t.Errorf("Failed to stat directory %s: %v", dir, err) + continue + } + if !info.IsDir() { + t.Errorf("Expected %s to be a directory", dir) + } + } +} + +func TestUnzipWithCorruptedEntries(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + zipData []byte + wantErr bool + }{ + { + name: "truncated zip", + zipData: []byte("PK\x03\x04"), // Valid zip header but truncated + wantErr: true, + }, + { + name: "corrupted central directory", + zipData: append([]byte("PK\x03\x04"), make([]byte, 100)...), + wantErr: true, + }, + { + name: "empty zip", + zipData: []byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + tempDir, err := os.MkdirTemp("", "zip_corrupted_*") + if err != nil { + t.Fatal("Failed to create temp dir:", err) + } + defer os.RemoveAll(tempDir) + + err = ziputil.UnzipToDir(tt.zipData, tempDir, func(string) bool { return true }) + if (err != nil) != tt.wantErr { + t.Errorf("UnzipToDir() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestUnzipWithDuplicateNames(t *testing.T) { + t.Parallel() + + // Create a zip with duplicate file names in different cases + files := map[string][]byte{ + "file.txt": []byte("content1"), + "FILE.txt": []byte("content2"), + "file.TXT": []byte("content3"), + "nested/file.txt": []byte("content4"), + } + + zipData, err := createTestZip(files) + if err != nil { + t.Fatal("Failed to create test zip:", err) + } + + tempDir, err := os.MkdirTemp("", "zip_duplicates_*") + if err != nil { + t.Fatal("Failed to create temp dir:", err) + } + defer os.RemoveAll(tempDir) + + // Test unzip + err = ziputil.UnzipToDir(zipData, tempDir, func(string) bool { return true }) + if err != nil { + t.Fatal("Failed to unzip:", err) + } + + // Verify files based on OS case sensitivity + entries, err := os.ReadDir(tempDir) + if err != nil { + t.Fatal("Failed to read directory:", err) + } + + // Count how many "file.txt" variants we find + count := 0 + for _, entry := range entries { + if !entry.IsDir() && strings.ToLower(entry.Name()) == "file.txt" { + count++ + } + } + + // On case-sensitive systems, we should find 3 files + // On case-insensitive systems, we should find 1 file + expectedCount := 3 + if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { + expectedCount = 1 + } + + if count != expectedCount { + t.Errorf("Expected %d file.txt variants, got %d", expectedCount, count) + } + + // Nested file should always exist + nestedPath := filepath.Join(tempDir, "nested", "file.txt") + if _, err := os.Stat(nestedPath); err != nil { + t.Errorf("Nested file.txt not found: %v", err) + } +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 7ef587a6..9cefb8c8 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -4,18 +4,18 @@ package e2e import ( "bytes" - "os/exec" "os" + "os/exec" "testing" "github.com/stretchr/testify/require" ) func TestInstallTerraform(t *testing.T) { - // - // Check the basic installation of a specific version. - // - tenvBin := os.Getenv("TENV_BIN") + // + // Check the basic installation of a specific version. + // + tenvBin := os.Getenv("TENV_BIN") cmd := exec.Command(tenvBin, "tf", "install", "1.10.5", "-v") @@ -25,105 +25,626 @@ func TestInstallTerraform(t *testing.T) { err := cmd.Run() - require.NoError(t, err, "Expected no error during the installation process") + require.NoError(t, err, "Expected no error during the installation process") require.Contains(t, out.String(), "Installing Terraform 1.10.5") require.Contains(t, out.String(), "Installation of Terraform 1.10.5 successful") } func TestTFenvVersionEnvVariable(t *testing.T) { - // - // Check that tenv detects the version from the env, - // but does not install it by default. - // - tenvBin := os.Getenv("TENV_BIN") + // + // Check that tenv detects the version from the env, + // but does not install it by default. + // + tenvBin := os.Getenv("TENV_BIN") - cmd := exec.Command(tenvBin, "tf", "detect") + cmd := exec.Command(tenvBin, "tf", "detect") - env := os.Environ() - env = append(env, "TFENV_TERRAFORM_VERSION=1.10.0") - cmd.Env = env + env := os.Environ() + env = append(env, "TFENV_TERRAFORM_VERSION=1.10.0") + cmd.Env = env - var out bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &out + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out - err := cmd.Run() + err := cmd.Run() - require.NoError(t, err, "Expected no error during the installation process") - require.NotContains(t, out.String(), "Installation of Terraform 1.10.0 successful") - require.Contains(t, out.String(), "Resolved version from TFENV_TERRAFORM_VERSION : 1.10.0") + require.NoError(t, err, "Expected no error during the installation process") + require.NotContains(t, out.String(), "Installation of Terraform 1.10.0 successful") + require.Contains(t, out.String(), "Resolved version from TFENV_TERRAFORM_VERSION : 1.10.0") } func TestTFenvVersionEnvVariableInstall(t *testing.T) { - // - // Check that tenv detects the version from the env, - // and install it if '-i' flag provided. - // - tenvBin := os.Getenv("TENV_BIN") - - cmd := exec.Command(tenvBin, "tf", "detect", "-i") + // + // Check that tenv detects the version from the env, + // and install it if '-i' flag provided. + // + tenvBin := os.Getenv("TENV_BIN") + + cmd := exec.Command(tenvBin, "tf", "detect", "-i") - env := os.Environ() - env = append(env, "TFENV_TERRAFORM_VERSION=1.10.0") - cmd.Env = env + env := os.Environ() + env = append(env, "TFENV_TERRAFORM_VERSION=1.10.0") + cmd.Env = env - var out bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &out + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out - err := cmd.Run() + err := cmd.Run() - require.NoError(t, err, "Expected no error during the installation process") - require.Contains(t, out.String(), "Installing Terraform 1.10.0") - require.Contains(t, out.String(), "Installation of Terraform 1.10.0 successful") + require.NoError(t, err, "Expected no error during the installation process") + require.Contains(t, out.String(), "Installing Terraform 1.10.0") + require.Contains(t, out.String(), "Installation of Terraform 1.10.0 successful") } func TestTFenvVersionLastUse(t *testing.T) { - // - // Check that the version file can be read by anyone. - // - tenvBin := os.Getenv("TENV_BIN") + // + // Check that the version file can be read by anyone. + // + tenvBin := os.Getenv("TENV_BIN") - env := os.Environ() - env = append(env, "TENV_ROOT=/usr/local/share/tenv", "TENV_AUTO_INSTALL=true") - var out bytes.Buffer - - cmd_install := exec.Command("sudo", "--preserve-env=TENV_ROOT,TENV_AUTO_INSTALL", tenvBin, "tofu", "use", "latest") - cmd_install.Env = env - cmd_install.Stdout = &out - cmd_install.Stderr = &out + env := os.Environ() + env = append(env, "TENV_ROOT=/usr/local/share/tenv", "TENV_AUTO_INSTALL=true") + var out bytes.Buffer + cmd_install := exec.Command("sudo", "--preserve-env=TENV_ROOT,TENV_AUTO_INSTALL", tenvBin, "tofu", "use", "latest") + cmd_install.Env = env + cmd_install.Stdout = &out + cmd_install.Stderr = &out - cmd_version := exec.Command("tofu", "--version") - cmd_version.Env = env - cmd_version.Stdout = &out - cmd_version.Stderr = &out + cmd_version := exec.Command("tofu", "--version") + cmd_version.Env = env + cmd_version.Stdout = &out + cmd_version.Stderr = &out - _ = cmd_install.Run() - err := cmd_version.Run() + _ = cmd_install.Run() + err := cmd_version.Run() - require.NoErrorf(t, err, "Expected no error during the version check. Output:\n%s", out.String()) + require.NoErrorf(t, err, "Expected no error during the version check. Output:\n%s", out.String()) } func TestTFenvTerragruntVersionDetect(t *testing.T) { - // - // Check that tenv detects the terragrunt version from the root.hcl file, - // but does not install it by default. - // - tenvBin := os.Getenv("TENV_BIN") + // + // Check that tenv detects the terragrunt version from the root.hcl file, + // but does not install it by default. + // + tenvBin := os.Getenv("TENV_BIN") - fileContent := `terragrunt_version_constraint = "0.69.1"` + fileContent := `terragrunt_version_constraint = "0.69.1"` _ = os.WriteFile("root.hcl", []byte(fileContent), 0644) - cmd := exec.Command(tenvBin, "tg", "detect") + cmd := exec.Command(tenvBin, "tg", "detect") + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err := cmd.Run() + + require.NoError(t, err, "Expected no error during the detect") + require.Contains(t, out.String(), "0.69.1") + require.NotContains(t, out.String(), "Installation of Terragrunt 0.69.1 successful") +} + +func TestTofuVersionFromFile(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .opentofu-version file + err := os.WriteFile(".opentofu-version", []byte("1.6.0"), 0644) + if err != nil { + t.Fatal("Failed to create .opentofu-version file:", err) + } + defer os.Remove(".opentofu-version") + + cmd := exec.Command(tenvBin, "tofu", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out - var out bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &out + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.6.0") +} + +func TestTofuVersionFromAsdf(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tool-versions file + toolVersions := `opentofu 1.6.1 +terraform 1.7.0 +` + err := os.WriteFile(".tool-versions", []byte(toolVersions), 0644) + if err != nil { + t.Fatal("Failed to create .tool-versions file:", err) + } + defer os.Remove(".tool-versions") + + cmd := exec.Command(tenvBin, "tofu", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.6.1") +} + +func TestTofuVersionFromTerragrunt(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test terragrunt.hcl file + hclContent := ` +terraform { + source = "..." + + # Configure OpenTofu version constraint + opentofu_version_constraint = "~> 1.6.0" +} +` + err := os.WriteFile("terragrunt.hcl", []byte(hclContent), 0644) + if err != nil { + t.Fatal("Failed to create terragrunt.hcl file:", err) + } + defer os.Remove("terragrunt.hcl") + + cmd := exec.Command(tenvBin, "tofu", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.6.0") +} + +func TestTofuVersionFromEnv(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + cmd := exec.Command(tenvBin, "tofu", "detect") + env := os.Environ() + env = append(env, "TOFUENV_TOFU_VERSION=1.6.2") + cmd.Env = env + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out - err := cmd.Run() + err := cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.6.2") +} + +func TestTofuVersionFromHCL(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tf file + hclContent := ` +terraform { + required_version = ">= 1.6.0" +} +` + err := os.WriteFile("main.tf", []byte(hclContent), 0644) + if err != nil { + t.Fatal("Failed to create main.tf file:", err) + } + defer os.Remove("main.tf") + + cmd := exec.Command(tenvBin, "tofu", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.6.0") +} + +func TestTofuVersionFromJSON(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tf.json file + jsonContent := `{ + "terraform": { + "required_version": ">= 1.6.0" + } +}` + err := os.WriteFile("main.tf.json", []byte(jsonContent), 0644) + if err != nil { + t.Fatal("Failed to create main.tf.json file:", err) + } + defer os.Remove("main.tf.json") + + cmd := exec.Command(tenvBin, "tofu", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.6.0") +} + +func TestAtmosVersionFromFile(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .atmos-version file + err := os.WriteFile(".atmos-version", []byte("1.130.0"), 0644) + if err != nil { + t.Fatal("Failed to create .atmos-version file:", err) + } + defer os.Remove(".atmos-version") + + cmd := exec.Command(tenvBin, "atmos", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.130.0") +} + +func TestAtmosVersionFromAsdf(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tool-versions file + toolVersions := `atmos 1.130.1 +terraform 1.7.0 +` + err := os.WriteFile(".tool-versions", []byte(toolVersions), 0644) + if err != nil { + t.Fatal("Failed to create .tool-versions file:", err) + } + defer os.Remove(".tool-versions") + + cmd := exec.Command(tenvBin, "atmos", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.130.1") +} + +func TestAtmosVersionFromEnv(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + cmd := exec.Command(tenvBin, "atmos", "detect") + env := os.Environ() + env = append(env, "ATMOS_VERSION=1.130.2") + cmd.Env = env + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err := cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.130.2") +} + +func TestAtmosInstallAndUse(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test installation of specific version + cmd := exec.Command(tenvBin, "atmos", "install", "1.130.0") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err := cmd.Run() + require.NoError(t, err, "Expected no error during installation") + require.Contains(t, out.String(), "Installation of Atmos 1.130.0 successful") + + // Test using the installed version + cmd = exec.Command(tenvBin, "atmos", "use", "1.130.0") + out.Reset() + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error when switching version") + require.Contains(t, out.String(), "Now using Atmos 1.130.0") +} + +func TestTerragruntVersionFromAsdf(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tool-versions file + toolVersions := `terragrunt 0.71.0 +terraform 1.7.0 +` + err := os.WriteFile(".tool-versions", []byte(toolVersions), 0644) + if err != nil { + t.Fatal("Failed to create .tool-versions file:", err) + } + defer os.Remove(".tool-versions") + + cmd := exec.Command(tenvBin, "tg", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "0.71.0") +} + +func TestTerragruntVersionFromEnv(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + cmd := exec.Command(tenvBin, "tg", "detect") + env := os.Environ() + env = append(env, "TG_VERSION=0.71.1") + cmd.Env = env + + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err := cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "0.71.1") +} + +func TestTerragruntVersionFromHCL(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test terragrunt.hcl file with version constraint + hclContent := ` +terragrunt_version_constraint = "~> 0.71.0" + +terraform { + source = "..." +} +` + err := os.WriteFile("terragrunt.hcl", []byte(hclContent), 0644) + if err != nil { + t.Fatal("Failed to create terragrunt.hcl file:", err) + } + defer os.Remove("terragrunt.hcl") + + cmd := exec.Command(tenvBin, "tg", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "0.71.0") +} + +func TestTerragruntVersionFromJSON(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test terragrunt.json file with version constraint + jsonContent := `{ + "terragrunt_version_constraint": "~> 0.71.0", + "terraform": { + "source": "..." + } +}` + err := os.WriteFile("terragrunt.json", []byte(jsonContent), 0644) + if err != nil { + t.Fatal("Failed to create terragrunt.json file:", err) + } + defer os.Remove("terragrunt.json") + + cmd := exec.Command(tenvBin, "tg", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "0.71.0") +} + +func TestTerragruntInstallAndUse(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test installation of specific version + cmd := exec.Command(tenvBin, "tg", "install", "0.71.0") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err := cmd.Run() + require.NoError(t, err, "Expected no error during installation") + require.Contains(t, out.String(), "Installation of Terragrunt 0.71.0 successful") + + // Test using the installed version + cmd = exec.Command(tenvBin, "tg", "use", "0.71.0") + out.Reset() + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error when switching version") + require.Contains(t, out.String(), "Now using Terragrunt 0.71.0") +} + +func TestTerraformVersionFromFile(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .terraform-version file + err := os.WriteFile(".terraform-version", []byte("1.7.0"), 0644) + if err != nil { + t.Fatal("Failed to create .terraform-version file:", err) + } + defer os.Remove(".terraform-version") + + cmd := exec.Command(tenvBin, "tf", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.7.0") +} + +func TestTerraformVersionFromTfswitchrc(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tfswitchrc file + err := os.WriteFile(".tfswitchrc", []byte("1.7.1"), 0644) + if err != nil { + t.Fatal("Failed to create .tfswitchrc file:", err) + } + defer os.Remove(".tfswitchrc") + + cmd := exec.Command(tenvBin, "tf", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.7.1") +} + +func TestTerraformVersionFromAsdf(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tool-versions file + toolVersions := `terraform 1.7.2 +terragrunt 0.71.0 +` + err := os.WriteFile(".tool-versions", []byte(toolVersions), 0644) + if err != nil { + t.Fatal("Failed to create .tool-versions file:", err) + } + defer os.Remove(".tool-versions") + + cmd := exec.Command(tenvBin, "tf", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.7.2") +} + +func TestTerraformVersionFromTerragruntHCL(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test terragrunt.hcl file with terraform version constraint + hclContent := ` +terraform { + source = "..." + + # Configure Terraform version constraint + terraform_version_constraint = "~> 1.7.0" +} +` + err := os.WriteFile("terragrunt.hcl", []byte(hclContent), 0644) + if err != nil { + t.Fatal("Failed to create terragrunt.hcl file:", err) + } + defer os.Remove("terragrunt.hcl") + + cmd := exec.Command(tenvBin, "tf", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.7.0") +} + +func TestTerraformVersionFromHCL(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tf file with required version + hclContent := ` +terraform { + required_version = ">= 1.7.0" +} +` + err := os.WriteFile("main.tf", []byte(hclContent), 0644) + if err != nil { + t.Fatal("Failed to create main.tf file:", err) + } + defer os.Remove("main.tf") + + cmd := exec.Command(tenvBin, "tf", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out + + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.7.0") +} + +func TestTerraformVersionFromJSON(t *testing.T) { + t.Parallel() + + tenvBin := os.Getenv("TENV_BIN") + + // Test .tf.json file with required version + jsonContent := `{ + "terraform": { + "required_version": ">= 1.7.0" + } +}` + err := os.WriteFile("main.tf.json", []byte(jsonContent), 0644) + if err != nil { + t.Fatal("Failed to create main.tf.json file:", err) + } + defer os.Remove("main.tf.json") + + cmd := exec.Command(tenvBin, "tf", "detect") + var out bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &out - require.NoError(t, err, "Expected no error during the detect") - require.Contains(t, out.String(), "0.69.1") - require.NotContains(t, out.String(), "Installation of Terragrunt 0.69.1 successful") + err = cmd.Run() + require.NoError(t, err, "Expected no error during version detection") + require.Contains(t, out.String(), "1.7.0") } diff --git a/versionmanager/builder/builder_test.go b/versionmanager/builder/builder_test.go new file mode 100644 index 00000000..36fb890a --- /dev/null +++ b/versionmanager/builder/builder_test.go @@ -0,0 +1,144 @@ +package builder + +import ( + "context" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/tofuutils/tenv/v4/config" + "github.com/tofuutils/tenv/v4/versionmanager" +) + +type mockRetriever struct { + versionmanager.ReleaseRetriever + installFunc func(ctx context.Context, version string, targetPath string) error + listVersionsFunc func(ctx context.Context) ([]string, error) +} + +func (m *mockRetriever) Install(ctx context.Context, version string, targetPath string) error { + return m.installFunc(ctx, version, targetPath) +} + +func (m *mockRetriever) ListVersions(ctx context.Context) ([]string, error) { + return m.listVersionsFunc(ctx) +} + +func TestBuildVersionManager(t *testing.T) { + tests := []struct { + name string + conf *config.Config + retriever *mockRetriever + wantErr bool + }{ + { + name: "successful build", + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + retriever: &mockRetriever{ + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return []string{"1.0.0", "1.1.0"}, nil + }, + }, + wantErr: false, + }, + { + name: "invalid config", + conf: nil, + retriever: &mockRetriever{ + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return []string{"1.0.0", "1.1.0"}, nil + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := BuildVersionManager(tt.conf, tt.retriever) + if (err != nil) != tt.wantErr { + t.Errorf("BuildVersionManager() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestBuildRetriever(t *testing.T) { + tests := []struct { + name string + conf *config.Config + wantErr bool + }{ + { + name: "successful build", + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + wantErr: false, + }, + { + name: "invalid config", + conf: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := BuildRetriever(tt.conf) + if (err != nil) != tt.wantErr { + t.Errorf("BuildRetriever() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestBuildVersionManagerWithRetriever(t *testing.T) { + tests := []struct { + name string + conf *config.Config + retriever *mockRetriever + wantErr bool + }{ + { + name: "successful build", + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + retriever: &mockRetriever{ + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return []string{"1.0.0", "1.1.0"}, nil + }, + }, + wantErr: false, + }, + { + name: "invalid config", + conf: nil, + retriever: &mockRetriever{ + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return []string{"1.0.0", "1.1.0"}, nil + }, + }, + wantErr: true, + }, + { + name: "invalid retriever", + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + retriever: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := BuildVersionManagerWithRetriever(tt.conf, tt.retriever) + if (err != nil) != tt.wantErr { + t.Errorf("BuildVersionManagerWithRetriever() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/versionmanager/lastuse/lastuse_test.go b/versionmanager/lastuse/lastuse_test.go new file mode 100644 index 00000000..29b1badd --- /dev/null +++ b/versionmanager/lastuse/lastuse_test.go @@ -0,0 +1,138 @@ +package lastuse + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/hashicorp/go-hclog" + "github.com/tofuutils/tenv/v4/config" +) + +func TestRead(t *testing.T) { + tests := []struct { + name string + path string + setup func(string) error + conf *config.Config + want time.Time + wantErr bool + }{ + { + name: "valid lastuse file", + path: "test", + setup: func(path string) error { + now := time.Now() + return os.WriteFile(filepath.Join(path, ".lastuse"), []byte(now.Format(time.RFC3339)), 0644) + }, + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + want: time.Now(), + wantErr: false, + }, + { + name: "invalid lastuse file", + path: "test", + setup: func(path string) error { + return os.WriteFile(filepath.Join(path, ".lastuse"), []byte("invalid"), 0644) + }, + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + want: time.Time{}, + wantErr: true, + }, + { + name: "missing lastuse file", + path: "test", + setup: func(path string) error { + return nil + }, + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + want: time.Time{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, tt.path) + if err := os.MkdirAll(path, 0755); err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + if err := tt.setup(path); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + got := Read(path, tt.conf) + if !tt.wantErr && got.IsZero() { + t.Errorf("Read() returned zero time when it shouldn't") + } + }) + } +} + +func TestWrite(t *testing.T) { + tests := []struct { + name string + path string + conf *config.Config + wantErr bool + }{ + { + name: "successful write", + path: "test", + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + wantErr: false, + }, + { + name: "invalid path", + path: "/invalid/path", + conf: &config.Config{ + Displayer: hclog.NewNullLogger(), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, tt.path) + if err := os.MkdirAll(path, 0755); err != nil { + t.Fatalf("Failed to create test directory: %v", err) + } + + err := Write(path, tt.conf) + if (err != nil) != tt.wantErr { + t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr) + } + + if !tt.wantErr { + // Verify the file was created + if _, err := os.Stat(filepath.Join(path, ".lastuse")); os.IsNotExist(err) { + t.Error("Write() did not create .lastuse file") + } + + // Verify the content is a valid timestamp + content, err := os.ReadFile(filepath.Join(path, ".lastuse")) + if err != nil { + t.Errorf("Failed to read .lastuse file: %v", err) + } + + _, err = time.Parse(time.RFC3339, string(content)) + if err != nil { + t.Errorf("Invalid timestamp in .lastuse file: %v", err) + } + } + }) + } +} diff --git a/versionmanager/manager_test.go b/versionmanager/manager_test.go new file mode 100644 index 00000000..2a31b853 --- /dev/null +++ b/versionmanager/manager_test.go @@ -0,0 +1,319 @@ +package versionmanager + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/hashicorp/go-hclog" + "github.com/tofuutils/tenv/v4/config" + "github.com/tofuutils/tenv/v4/versionmanager/semantic/parser/iac" + "github.com/tofuutils/tenv/v4/versionmanager/semantic/types" +) + +type mockRetriever struct { + installFunc func(ctx context.Context, version string, targetPath string) error + listVersionsFunc func(ctx context.Context) ([]string, error) +} + +func (m *mockRetriever) Install(ctx context.Context, version string, targetPath string) error { + return m.installFunc(ctx, version, targetPath) +} + +func (m *mockRetriever) ListVersions(ctx context.Context) ([]string, error) { + return m.listVersionsFunc(ctx) +} + +func TestVersionManager_Detect(t *testing.T) { + tests := []struct { + name string + conf *config.Config + retriever *mockRetriever + proxyCall bool + wantVersion string + wantErr bool + }{ + { + name: "successful detection", + conf: &config.Config{ + SkipInstall: true, + Displayer: hclog.NewNullLogger(), + }, + retriever: &mockRetriever{ + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return []string{"1.0.0", "1.1.0"}, nil + }, + }, + proxyCall: false, + wantVersion: "1.1.0", + wantErr: false, + }, + { + name: "error in retriever", + conf: &config.Config{ + SkipInstall: true, + Displayer: hclog.NewNullLogger(), + }, + retriever: &mockRetriever{ + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return nil, errors.New("retriever error") + }, + }, + proxyCall: false, + wantVersion: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vm := Make(tt.conf, "TEST", "test", []iac.ExtDescription{}, tt.retriever, []types.VersionFile{}) + got, err := vm.Detect(context.Background(), tt.proxyCall) + if (err != nil) != tt.wantErr { + t.Errorf("VersionManager.Detect() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.wantVersion { + t.Errorf("VersionManager.Detect() = %v, want %v", got, tt.wantVersion) + } + }) + } +} + +func TestVersionManager_InstallPath(t *testing.T) { + tests := []struct { + name string + conf *config.Config + folder string + want string + wantErr bool + }{ + { + name: "successful path creation", + conf: &config.Config{ + RootPath: t.TempDir(), + }, + folder: "test", + want: filepath.Join(t.TempDir(), "test"), + wantErr: false, + }, + { + name: "invalid root path", + conf: &config.Config{ + RootPath: "/invalid/path", + }, + folder: "test", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vm := Make(tt.conf, "TEST", tt.folder, []iac.ExtDescription{}, &mockRetriever{}, []types.VersionFile{}) + got, err := vm.InstallPath() + if (err != nil) != tt.wantErr { + t.Errorf("VersionManager.InstallPath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want && !tt.wantErr { + t.Errorf("VersionManager.InstallPath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVersionManager_LocalSet(t *testing.T) { + tests := []struct { + name string + conf *config.Config + folder string + setup func(string) error + want map[string]struct{} + }{ + { + name: "empty directory", + conf: &config.Config{ + RootPath: t.TempDir(), + Displayer: hclog.NewNullLogger(), + }, + folder: "test", + setup: func(path string) error { return nil }, + want: map[string]struct{}{}, + }, + { + name: "with versions", + conf: &config.Config{ + RootPath: t.TempDir(), + Displayer: hclog.NewNullLogger(), + }, + folder: "test", + setup: func(path string) error { + versions := []string{"1.0.0", "1.1.0"} + for _, v := range versions { + if err := os.MkdirAll(filepath.Join(path, v), 0755); err != nil { + return err + } + } + return nil + }, + want: map[string]struct{}{ + "1.0.0": {}, + "1.1.0": {}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + installPath := filepath.Join(tt.conf.RootPath, tt.folder) + if err := tt.setup(installPath); err != nil { + t.Fatalf("setup failed: %v", err) + } + + vm := Make(tt.conf, "TEST", tt.folder, []iac.ExtDescription{}, &mockRetriever{}, []types.VersionFile{}) + got := vm.LocalSet() + + if len(got) != len(tt.want) { + t.Errorf("VersionManager.LocalSet() = %v, want %v", got, tt.want) + } + + for version := range tt.want { + if _, ok := got[version]; !ok { + t.Errorf("VersionManager.LocalSet() missing version %v", version) + } + } + }) + } +} + +func TestVersionManager_ListLocal(t *testing.T) { + tests := []struct { + name string + conf *config.Config + folder string + setup func(string) error + reverseOrder bool + want []DatedVersion + wantErr bool + }{ + { + name: "empty directory", + conf: &config.Config{ + RootPath: t.TempDir(), + Displayer: hclog.NewNullLogger(), + }, + folder: "test", + setup: func(path string) error { return nil }, + reverseOrder: false, + want: []DatedVersion{}, + wantErr: false, + }, + { + name: "with versions", + conf: &config.Config{ + RootPath: t.TempDir(), + Displayer: hclog.NewNullLogger(), + }, + folder: "test", + setup: func(path string) error { + versions := []string{"1.0.0", "1.1.0"} + for _, v := range versions { + if err := os.MkdirAll(filepath.Join(path, v), 0755); err != nil { + return err + } + } + return nil + }, + reverseOrder: false, + want: []DatedVersion{ + {Version: "1.0.0", UseDate: time.Time{}}, + {Version: "1.1.0", UseDate: time.Time{}}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + installPath := filepath.Join(tt.conf.RootPath, tt.folder) + if err := tt.setup(installPath); err != nil { + t.Fatalf("setup failed: %v", err) + } + + vm := Make(tt.conf, "TEST", tt.folder, []iac.ExtDescription{}, &mockRetriever{}, []types.VersionFile{}) + got, err := vm.ListLocal(tt.reverseOrder) + if (err != nil) != tt.wantErr { + t.Errorf("VersionManager.ListLocal() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if len(got) != len(tt.want) { + t.Errorf("VersionManager.ListLocal() = %v, want %v", got, tt.want) + } + + for i, version := range got { + if version.Version != tt.want[i].Version { + t.Errorf("VersionManager.ListLocal() version = %v, want %v", version.Version, tt.want[i].Version) + } + } + }) + } +} + +func TestVersionManager_ListRemote(t *testing.T) { + tests := []struct { + name string + retriever *mockRetriever + reverseOrder bool + want []string + wantErr bool + }{ + { + name: "successful list", + retriever: &mockRetriever{ + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return []string{"1.0.0", "1.1.0"}, nil + }, + }, + reverseOrder: false, + want: []string{"1.0.0", "1.1.0"}, + wantErr: false, + }, + { + name: "error in retriever", + retriever: &mockRetriever{ + listVersionsFunc: func(ctx context.Context) ([]string, error) { + return nil, errors.New("retriever error") + }, + }, + reverseOrder: false, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vm := Make(&config.Config{}, "TEST", "test", []iac.ExtDescription{}, tt.retriever, []types.VersionFile{}) + got, err := vm.ListRemote(context.Background(), tt.reverseOrder) + if (err != nil) != tt.wantErr { + t.Errorf("VersionManager.ListRemote() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if len(got) != len(tt.want) { + t.Errorf("VersionManager.ListRemote() = %v, want %v", got, tt.want) + } + + for i, version := range got { + if version != tt.want[i] { + t.Errorf("VersionManager.ListRemote() version = %v, want %v", version, tt.want[i]) + } + } + }) + } +} diff --git a/versionmanager/prefix_test.go b/versionmanager/prefix_test.go new file mode 100644 index 00000000..9cd50d1c --- /dev/null +++ b/versionmanager/prefix_test.go @@ -0,0 +1,106 @@ +package versionmanager + +import ( + "testing" + + "github.com/tofuutils/tenv/v4/config/envname" +) + +func TestEnvPrefix_Version(t *testing.T) { + tests := []struct { + name string + prefix EnvPrefix + expected string + }{ + { + name: "empty prefix", + prefix: "", + expected: envname.VersionSuffix, + }, + { + name: "non-empty prefix", + prefix: "TF_", + expected: "TF_" + envname.VersionSuffix, + }, + { + name: "underscore prefix", + prefix: "TF_", + expected: "TF_" + envname.VersionSuffix, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.prefix.Version() + if result != tt.expected { + t.Errorf("Version() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestEnvPrefix_constraint(t *testing.T) { + tests := []struct { + name string + prefix EnvPrefix + expected string + }{ + { + name: "empty prefix", + prefix: "", + expected: envname.DefaultConstraintSuffix, + }, + { + name: "non-empty prefix", + prefix: "TF_", + expected: "TF_" + envname.DefaultConstraintSuffix, + }, + { + name: "underscore prefix", + prefix: "TF_", + expected: "TF_" + envname.DefaultConstraintSuffix, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.prefix.constraint() + if result != tt.expected { + t.Errorf("constraint() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestEnvPrefix_defaultVersion(t *testing.T) { + tests := []struct { + name string + prefix EnvPrefix + expected string + }{ + { + name: "empty prefix", + prefix: "", + expected: envname.DefaultVersionSuffix, + }, + { + name: "non-empty prefix", + prefix: "TF_", + expected: "TF_" + envname.DefaultVersionSuffix, + }, + { + name: "underscore prefix", + prefix: "TF_", + expected: "TF_" + envname.DefaultVersionSuffix, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.prefix.defaultVersion() + if result != tt.expected { + t.Errorf("defaultVersion() = %v, want %v", result, tt.expected) + } + }) + } +} diff --git a/versionmanager/proxy/light/light_test.go b/versionmanager/proxy/light/light_test.go new file mode 100644 index 00000000..6a4e469f --- /dev/null +++ b/versionmanager/proxy/light/light_test.go @@ -0,0 +1,46 @@ +package lightproxy + +import ( + "os" + "testing" + + "github.com/tofuutils/tenv/v4/config/cmdconst" +) + +func TestExec(t *testing.T) { + tests := []struct { + name string + toolName string + }{ + { + name: "valid tool name", + toolName: cmdconst.TofuName, + }, + { + name: "invalid tool name", + toolName: "nonexistent", + }, + { + name: "empty tool name", + toolName: "", + }, + } + + // Save original args + originalArgs := os.Args + defer func() { + os.Args = originalArgs + }() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set test args + os.Args = []string{"test", "--version"} + + // Execute command + Exec(tt.toolName) + // Note: Since Exec calls os.Exit, we can't actually test the return value + // This test is mainly to ensure the function doesn't panic + }) + } +} diff --git a/versionmanager/proxy/proxy_test.go b/versionmanager/proxy/proxy_test.go new file mode 100644 index 00000000..f5a6b956 --- /dev/null +++ b/versionmanager/proxy/proxy_test.go @@ -0,0 +1,216 @@ +package proxy + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/hcl/v2/hclparse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tofuutils/tenv/v4/config" + "github.com/tofuutils/tenv/v4/versionmanager" + "github.com/tofuutils/tenv/v4/versionmanager/builder" +) + +type mockVersionManager struct { + detectFunc func(ctx context.Context, allowDefault bool) (string, error) + installPathFunc func() (string, error) +} + +func (m *mockVersionManager) Detect(ctx context.Context, allowDefault bool) (string, error) { + return m.detectFunc(ctx, allowDefault) +} + +func (m *mockVersionManager) InstallPath() (string, error) { + return m.installPathFunc() +} + +func (m *mockVersionManager) Evaluate(ctx context.Context, requestedVersion string, proxyCall bool) (string, error) { + return "", nil +} + +func (m *mockVersionManager) Install(ctx context.Context, requestedVersion string) error { + return nil +} + +func (m *mockVersionManager) InstallMultiple(ctx context.Context, versions []string) error { + return nil +} + +func (m *mockVersionManager) ListLocal(reverseOrder bool) ([]versionmanager.DatedVersion, error) { + return nil, nil +} + +func (m *mockVersionManager) ListRemote(ctx context.Context, reverseOrder bool) ([]string, error) { + return nil, nil +} + +func (m *mockVersionManager) LocalSet() map[string]struct{} { + return nil +} + +func (m *mockVersionManager) ReadDefaultConstraint() string { + return "" +} + +func (m *mockVersionManager) ReadDefaultVersion() string { + return "" +} + +func (m *mockVersionManager) Resolve(requestedVersion string) (string, error) { + return "", nil +} + +func (m *mockVersionManager) ResolveWithVersionFiles() (string, error) { + return "", nil +} + +func TestExecPath(t *testing.T) { + tests := []struct { + name string + installPath string + version string + execName string + expected string + }{ + { + name: "basic path", + installPath: "/tmp/install", + version: "1.0.0", + execName: "terraform", + expected: filepath.Join("/tmp/install", "1.0.0", "terraform"), + }, + { + name: "empty version", + installPath: "/tmp/install", + version: "", + execName: "terraform", + expected: filepath.Join("/tmp/install", "", "terraform"), + }, + { + name: "empty exec name", + installPath: "/tmp/install", + version: "1.0.0", + execName: "", + expected: filepath.Join("/tmp/install", "1.0.0", ""), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conf := &config.Config{} + result := ExecPath(tt.installPath, tt.version, tt.execName, conf) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestUpdateWorkPath(t *testing.T) { + tests := []struct { + name string + conf *config.Config + cmdArgs []string + expected string + }{ + { + name: "no chdir flag", + conf: &config.Config{WorkPath: "/original"}, + cmdArgs: []string{"plan", "-var=foo=bar"}, + expected: "/original", + }, + { + name: "with chdir flag", + conf: &config.Config{WorkPath: "/original"}, + cmdArgs: []string{"-chdir=/new/path", "plan"}, + expected: "/new/path", + }, + { + name: "multiple chdir flags", + conf: &config.Config{WorkPath: "/original"}, + cmdArgs: []string{"-chdir=/first", "-chdir=/second", "plan"}, + expected: "/first", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updateWorkPath(tt.conf, tt.cmdArgs) + assert.Equal(t, tt.expected, tt.conf.WorkPath) + }) + } +} + +func TestExec(t *testing.T) { + // Create a temporary directory for testing + tempDir, err := os.MkdirTemp("", "tenv-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + tests := []struct { + name string + conf *config.Config + builderFunc builder.Func + hclParser *hclparse.Parser + execName string + cmdArgs []string + expectedError bool + }{ + { + name: "successful execution", + conf: &config.Config{ + WorkPath: tempDir, + }, + builderFunc: func(conf *config.Config, parser *hclparse.Parser) versionmanager.VersionManager { + return versionmanager.Make(conf, "TF_", "terraform", nil, nil, nil) + }, + hclParser: hclparse.NewParser(), + execName: "terraform", + cmdArgs: []string{"version"}, + expectedError: false, + }, + { + name: "detection failure", + conf: &config.Config{ + WorkPath: tempDir, + }, + builderFunc: func(conf *config.Config, parser *hclparse.Parser) versionmanager.VersionManager { + return versionmanager.Make(conf, "TF_", "terraform", nil, nil, nil) + }, + hclParser: hclparse.NewParser(), + execName: "terraform", + cmdArgs: []string{"version"}, + expectedError: true, + }, + { + name: "install path failure", + conf: &config.Config{ + WorkPath: tempDir, + }, + builderFunc: func(conf *config.Config, parser *hclparse.Parser) versionmanager.VersionManager { + return versionmanager.Make(conf, "TF_", "terraform", nil, nil, nil) + }, + hclParser: hclparse.NewParser(), + execName: "terraform", + cmdArgs: []string{"version"}, + expectedError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Since Exec calls os.Exit, we need to run it in a separate process + if tt.expectedError { + // For error cases, we just verify that the function would exit + // This is a simplified test - in a real scenario, you might want to + // use a more sophisticated approach to test os.Exit behavior + return + } + + // For successful cases, we can test the path construction + Exec(tt.conf, tt.builderFunc, tt.hclParser, tt.execName, tt.cmdArgs) + }) + } +} diff --git a/versionmanager/semantic/parser/asdf/asdfparser_test.go b/versionmanager/semantic/parser/asdf/asdfparser_test.go index 7c6d8bf4..685d879b 100644 --- a/versionmanager/semantic/parser/asdf/asdfparser_test.go +++ b/versionmanager/semantic/parser/asdf/asdfparser_test.go @@ -19,44 +19,575 @@ package asdfparser import ( - "bytes" _ "embed" + "os" + "path/filepath" + "strings" + "sync" "testing" + "github.com/hashicorp/go-hclog" + "github.com/tofuutils/tenv/v4/config" "github.com/tofuutils/tenv/v4/config/cmdconst" - "github.com/tofuutils/tenv/v4/pkg/loghelper" ) //go:embed testdata/.tool-versions var toolFileData []byte +// mockDisplayer implements loghelper.Displayer for testing +type mockDisplayer struct{} + +func (m *mockDisplayer) Display(string) {} +func (m *mockDisplayer) Log(hclog.Level, string, ...interface{}) {} +func (m *mockDisplayer) IsDebug() bool { return false } +func (m *mockDisplayer) IsTrace() bool { return false } +func (m *mockDisplayer) Flush(bool) {} + +func TestRetrieveTofuVersion(t *testing.T) { + t.Parallel() + testRetrieveVersion(t, cmdconst.OpentofuName, RetrieveTofuVersion) +} + +func TestRetrieveTfVersion(t *testing.T) { + t.Parallel() + testRetrieveVersion(t, cmdconst.TerraformName, RetrieveTfVersion) +} + +func TestRetrieveTgVersion(t *testing.T) { + t.Parallel() + testRetrieveVersion(t, cmdconst.TerragruntName, RetrieveTgVersion) +} + +func TestRetrieveAtmosVersion(t *testing.T) { + t.Parallel() + testRetrieveVersion(t, cmdconst.AtmosName, RetrieveAtmosVersion) +} + +func testRetrieveVersion(t *testing.T, toolName string, retrieveFunc func(string, *config.Config) (string, error)) { + tests := []struct { + name string + content string + expectedResult string + expectError bool + }{ + { + name: "valid version", + content: toolName + " 1.0.0", + expectedResult: "1.0.0", + }, + { + name: "version with comment", + content: toolName + " 1.0.0 # comment", + expectedResult: "1.0.0", + }, + { + name: "version with inline comment", + content: toolName + " 1.0.0#comment", + expectedResult: "1.0.0", + }, + { + name: "multiple tools", + content: "nodejs 14.0.0\n" + toolName + " 1.0.0\npython 3.8.0", + expectedResult: "1.0.0", + }, + { + name: "empty file", + content: "", + expectedResult: "", + }, + { + name: "comments only", + content: "# comment\n# another comment", + expectedResult: "", + }, + { + name: "tool not found", + content: "nodejs 14.0.0\npython 3.8.0", + expectedResult: "", + }, + { + name: "multiple versions", + content: toolName + " 1.0.0\n" + toolName + " 2.0.0", + expectedResult: "1.0.0", + }, + { + name: "version with spaces", + content: toolName + " 1.0.0 ", + expectedResult: "1.0.0", + }, + { + name: "version with tabs", + content: toolName + "\t1.0.0\t", + expectedResult: "1.0.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, ToolFileName) + + // Create test file + err := os.WriteFile(filePath, []byte(tt.content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := retrieveFunc(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + + if result != tt.expectedResult { + t.Errorf("expected %s but got %s", tt.expectedResult, result) + } + }) + } +} + +func TestConcurrentAccess(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, ToolFileName) + + // Create test file + content := "terraform 1.0.0\nterragrunt 1.2.0\nopentofu 1.3.0\natmos 1.4.0" + err := os.WriteFile(filePath, []byte(content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Number of concurrent goroutines + numGoroutines := 10 + var wg sync.WaitGroup + wg.Add(numGoroutines) + + // Run concurrent tests + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + // Test all version retrieval functions + funcs := []struct { + name string + fn func(string, *config.Config) (string, error) + }{ + {"RetrieveTfVersion", RetrieveTfVersion}, + {"RetrieveTgVersion", RetrieveTgVersion}, + {"RetrieveTofuVersion", RetrieveTofuVersion}, + {"RetrieveAtmosVersion", RetrieveAtmosVersion}, + } + + for _, f := range funcs { + result, err := f.fn(filePath, conf) + if err != nil { + t.Error(err) + return + } + expected := "" + switch f.name { + case "RetrieveTfVersion": + expected = "1.0.0" + case "RetrieveTgVersion": + expected = "1.2.0" + case "RetrieveTofuVersion": + expected = "1.3.0" + case "RetrieveAtmosVersion": + expected = "1.4.0" + } + if result != expected { + t.Errorf("for %s, expected %s but got %s", f.name, expected, result) + } + } + }() + } + + wg.Wait() +} + +func TestFileErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(string) error + expectError bool + }{ + { + name: "non-existent file", + setup: func(dir string) error { + return nil // No setup needed, file doesn't exist + }, + expectError: false, // Should return empty string, not error + }, + { + name: "unreadable file", + setup: func(dir string) error { + filePath := filepath.Join(dir, ToolFileName) + if err := os.WriteFile(filePath, []byte("terraform 1.0.0"), 0600); err != nil { + return err + } + return os.Chmod(filePath, 0000) + }, + expectError: true, + }, + { + name: "directory instead of file", + setup: func(dir string) error { + filePath := filepath.Join(dir, ToolFileName) + return os.Mkdir(filePath, 0700) + }, + expectError: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Apply setup + if err := tt.setup(tempDir); err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + filePath := filepath.Join(tempDir, ToolFileName) + _, err := RetrieveTfVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + }) + } +} + func TestParseVersionFromToolFileReader(t *testing.T) { t.Parallel() - t.Run("BasicLine", func(t *testing.T) { - t.Parallel() + tests := []struct { + name string + content string + toolName string + expectedResult string + }{ + { + name: "valid version", + content: "terraform 1.0.0", + toolName: "terraform", + expectedResult: "1.0.0", + }, + { + name: "version with comment", + content: "terraform 1.0.0 # comment", + toolName: "terraform", + expectedResult: "1.0.0", + }, + { + name: "version with inline comment", + content: "terraform 1.0.0#comment", + toolName: "terraform", + expectedResult: "1.0.0", + }, + { + name: "multiple tools", + content: "nodejs 14.0.0\nterraform 1.0.0\npython 3.8.0", + toolName: "terraform", + expectedResult: "1.0.0", + }, + { + name: "empty content", + content: "", + toolName: "terraform", + expectedResult: "", + }, + { + name: "comments only", + content: "# comment\n# another comment", + toolName: "terraform", + expectedResult: "", + }, + { + name: "tool not found", + content: "nodejs 14.0.0\npython 3.8.0", + toolName: "terraform", + expectedResult: "", + }, + { + name: "multiple versions", + content: "terraform 1.0.0\nterraform 2.0.0", + toolName: "terraform", + expectedResult: "1.0.0", + }, + { + name: "version with spaces", + content: "terraform 1.0.0 ", + toolName: "terraform", + expectedResult: "1.0.0", + }, + { + name: "version with tabs", + content: "terraform\t1.0.0\t", + toolName: "terraform", + expectedResult: "1.0.0", + }, + } - version := parseVersionFromToolFileReader("", bytes.NewReader(toolFileData), cmdconst.AtmosName, loghelper.InertDisplayer) - if version != "1.130.0" { - t.Fatal("Unexpected version : ", version) - } - }) + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create reader + reader := strings.NewReader(tt.content) + + // Create displayer + displayer := &mockDisplayer{} + + // Run test + result := parseVersionFromToolFileReader("test.tool-versions", reader, tt.toolName, displayer) + + if result != tt.expectedResult { + t.Errorf("expected %s but got %s", tt.expectedResult, result) + } + }) + } +} + +func TestFileEncodings(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content []byte + expectedResult string + expectError bool + }{ + { + name: "UTF-8", + content: []byte("terraform 1.0.0"), + expectedResult: "1.0.0", + }, + { + name: "UTF-8 with BOM", + content: append([]byte{0xEF, 0xBB, 0xBF}, []byte("terraform 1.0.0")...), + expectedResult: "1.0.0", + }, + { + name: "UTF-16", + content: append([]byte{0xFF, 0xFE}, []byte("terraform 1.0.0")...), + expectError: true, + }, + { + name: "ASCII", + content: []byte("terraform 1.0.0"), + expectedResult: "1.0.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, ToolFileName) + + // Create test file + err := os.WriteFile(filePath, tt.content, 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveTfVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + + if result != tt.expectedResult { + t.Errorf("expected %s but got %s", tt.expectedResult, result) + } + }) + } +} + +func TestLargeFiles(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, ToolFileName) - t.Run("LineWithComment", func(t *testing.T) { - t.Parallel() + // Create a large file with version constraint + content := make([]byte, 10*1024*1024) // 10MB + copy(content, []byte("terraform 1.0.0")) - version := parseVersionFromToolFileReader("", bytes.NewReader(toolFileData), cmdconst.OpentofuName, loghelper.InertDisplayer) - if version != "1.8.7" { - t.Fatal("Unexpected version : ", version) + // Create test file + err := os.WriteFile(filePath, content, 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveTfVersion(filePath, conf) + if err != nil { + t.Fatal(err) + } + + if result != "1.0.0" { + t.Errorf("expected 1.0.0 but got %s", result) + } +} + +func TestSymbolicLinks(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + originalPath := filepath.Join(tempDir, "original.tool-versions") + linkPath := filepath.Join(tempDir, ToolFileName) + + // Create original file + content := "terraform 1.0.0" + err := os.WriteFile(originalPath, []byte(content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create symbolic link + err = os.Symlink(originalPath, linkPath) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveTfVersion(linkPath, conf) + if err != nil { + t.Fatal(err) + } + + if result != "1.0.0" { + t.Errorf("expected 1.0.0 but got %s", result) + } +} + +func TestMultipleFiles(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Create multiple files with different version constraints + files := []struct { + name string + content string + }{ + { + name: ToolFileName, + content: "terraform 1.0.0", + }, + { + name: "other.tool-versions", + content: "terraform 1.1.0", + }, + { + name: "config.tool-versions", + content: "nodejs 14.0.0", + }, + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Create and test each file + for _, file := range files { + filePath := filepath.Join(tempDir, file.name) + err := os.WriteFile(filePath, []byte(file.content), 0600) + if err != nil { + t.Fatal(err) } - }) - t.Run("LineFallback", func(t *testing.T) { - t.Parallel() + result, err := RetrieveTfVersion(filePath, conf) + if err != nil { + t.Fatal(err) + } + + expected := "" + if file.name != "config.tool-versions" { + expected = "1.0.0" + if file.name == "other.tool-versions" { + expected = "1.1.0" + } + } - version := parseVersionFromToolFileReader("", bytes.NewReader(toolFileData), cmdconst.TerragruntName, loghelper.InertDisplayer) - if version != "0.71.1" { - t.Fatal("Unexpected version : ", version) + if result != expected { + t.Errorf("for file %s, expected %s but got %s", file.name, expected, result) } - }) + } } diff --git a/versionmanager/semantic/parser/flat/flatparser_test.go b/versionmanager/semantic/parser/flat/flatparser_test.go new file mode 100644 index 00000000..234384a6 --- /dev/null +++ b/versionmanager/semantic/parser/flat/flatparser_test.go @@ -0,0 +1,565 @@ +/* + * + * Copyright 2024 tofuutils authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package flatparser + +import ( + "os" + "path/filepath" + "sync" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/tofuutils/tenv/v4/config" + "github.com/tofuutils/tenv/v4/pkg/loghelper" + "github.com/tofuutils/tenv/v4/versionmanager/semantic/types" +) + +// mockDisplayer implements loghelper.Displayer for testing +type mockDisplayer struct{} + +func (m *mockDisplayer) Display(string) {} +func (m *mockDisplayer) Log(hclog.Level, string, ...interface{}) {} +func (m *mockDisplayer) IsDebug() bool { return false } +func (m *mockDisplayer) IsTrace() bool { return false } +func (m *mockDisplayer) Flush(bool) {} + +func TestRetrieve(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + displayMsg func(loghelper.Displayer, string, string) string + expectedResult string + expectError bool + }{ + { + name: "valid version with NoMsg", + content: "1.0.0", + displayMsg: NoMsg, + expectedResult: "1.0.0", + }, + { + name: "valid version with DisplayDetectionInfo", + content: "1.0.0", + displayMsg: types.DisplayDetectionInfo, + expectedResult: "1.0.0", + }, + { + name: "version with spaces", + content: " 1.0.0 ", + displayMsg: NoMsg, + expectedResult: "1.0.0", + }, + { + name: "version with tabs", + content: "\t1.0.0\t", + displayMsg: NoMsg, + expectedResult: "1.0.0", + }, + { + name: "empty file", + content: "", + displayMsg: NoMsg, + expectedResult: "", + }, + { + name: "whitespace only", + content: " \t ", + displayMsg: NoMsg, + expectedResult: "", + }, + { + name: "newlines only", + content: "\n\n\n", + displayMsg: NoMsg, + expectedResult: "", + }, + { + name: "version with newlines", + content: "\n1.0.0\n", + displayMsg: NoMsg, + expectedResult: "1.0.0", + }, + { + name: "version with comments", + content: "1.0.0 # comment", + displayMsg: NoMsg, + expectedResult: "1.0.0 # comment", + }, + { + name: "version with multiple lines", + content: "1.0.0\n2.0.0", + displayMsg: NoMsg, + expectedResult: "1.0.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "version.txt") + + // Create test file + err := os.WriteFile(filePath, []byte(tt.content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := Retrieve(filePath, conf, tt.displayMsg) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + + if result != tt.expectedResult { + t.Errorf("expected %s but got %s", tt.expectedResult, result) + } + }) + } +} + +func TestRetrieveVersion(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + expectedResult string + expectError bool + }{ + { + name: "valid version", + content: "1.0.0", + expectedResult: "1.0.0", + }, + { + name: "version with spaces", + content: " 1.0.0 ", + expectedResult: "1.0.0", + }, + { + name: "version with tabs", + content: "\t1.0.0\t", + expectedResult: "1.0.0", + }, + { + name: "empty file", + content: "", + expectedResult: "", + }, + { + name: "whitespace only", + content: " \t ", + expectedResult: "", + }, + { + name: "newlines only", + content: "\n\n\n", + expectedResult: "", + }, + { + name: "version with newlines", + content: "\n1.0.0\n", + expectedResult: "1.0.0", + }, + { + name: "version with comments", + content: "1.0.0 # comment", + expectedResult: "1.0.0 # comment", + }, + { + name: "version with multiple lines", + content: "1.0.0\n2.0.0", + expectedResult: "1.0.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "version.txt") + + // Create test file + err := os.WriteFile(filePath, []byte(tt.content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + + if result != tt.expectedResult { + t.Errorf("expected %s but got %s", tt.expectedResult, result) + } + }) + } +} + +func TestConcurrentAccess(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "version.txt") + + // Create test file + content := "1.0.0" + err := os.WriteFile(filePath, []byte(content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Number of concurrent goroutines + numGoroutines := 10 + var wg sync.WaitGroup + wg.Add(numGoroutines) + + // Run concurrent tests + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + // Test both Retrieve and RetrieveVersion + funcs := []struct { + name string + fn func(string, *config.Config) (string, error) + }{ + {"RetrieveVersion", RetrieveVersion}, + } + + for _, f := range funcs { + result, err := f.fn(filePath, conf) + if err != nil { + t.Error(err) + return + } + if result != "1.0.0" { + t.Errorf("for %s, expected 1.0.0 but got %s", f.name, result) + } + } + }() + } + + wg.Wait() +} + +func TestFileErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(string) error + expectError bool + }{ + { + name: "non-existent file", + setup: func(dir string) error { + return nil // No setup needed, file doesn't exist + }, + expectError: false, // Should return empty string, not error + }, + { + name: "unreadable file", + setup: func(dir string) error { + filePath := filepath.Join(dir, "version.txt") + if err := os.WriteFile(filePath, []byte("1.0.0"), 0600); err != nil { + return err + } + return os.Chmod(filePath, 0000) + }, + expectError: true, + }, + { + name: "directory instead of file", + setup: func(dir string) error { + filePath := filepath.Join(dir, "version.txt") + return os.Mkdir(filePath, 0700) + }, + expectError: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Apply setup + if err := tt.setup(tempDir); err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + filePath := filepath.Join(tempDir, "version.txt") + _, err := RetrieveVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + }) + } +} + +func TestFileEncodings(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content []byte + expectedResult string + expectError bool + }{ + { + name: "UTF-8", + content: []byte("1.0.0"), + expectedResult: "1.0.0", + }, + { + name: "UTF-8 with BOM", + content: append([]byte{0xEF, 0xBB, 0xBF}, []byte("1.0.0")...), + expectedResult: "1.0.0", + }, + { + name: "UTF-16", + content: append([]byte{0xFF, 0xFE}, []byte("1.0.0")...), + expectError: true, + }, + { + name: "ASCII", + content: []byte("1.0.0"), + expectedResult: "1.0.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "version.txt") + + // Create test file + err := os.WriteFile(filePath, tt.content, 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + + if result != tt.expectedResult { + t.Errorf("expected %s but got %s", tt.expectedResult, result) + } + }) + } +} + +func TestLargeFiles(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "version.txt") + + // Create a large file with version constraint + content := make([]byte, 10*1024*1024) // 10MB + copy(content, []byte("1.0.0")) + + // Create test file + err := os.WriteFile(filePath, content, 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveVersion(filePath, conf) + if err != nil { + t.Fatal(err) + } + + if result != "1.0.0" { + t.Errorf("expected 1.0.0 but got %s", result) + } +} + +func TestSymbolicLinks(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + originalPath := filepath.Join(tempDir, "original.txt") + linkPath := filepath.Join(tempDir, "version.txt") + + // Create original file + content := "1.0.0" + err := os.WriteFile(originalPath, []byte(content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create symbolic link + err = os.Symlink(originalPath, linkPath) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveVersion(linkPath, conf) + if err != nil { + t.Fatal(err) + } + + if result != "1.0.0" { + t.Errorf("expected 1.0.0 but got %s", result) + } +} + +func TestMultipleFiles(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Create multiple files with different version constraints + files := []struct { + name string + content string + }{ + { + name: "version.txt", + content: "1.0.0", + }, + { + name: "other.txt", + content: "1.1.0", + }, + { + name: "config.txt", + content: "2.0.0", + }, + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Create and test each file + for _, file := range files { + filePath := filepath.Join(tempDir, file.name) + err := os.WriteFile(filePath, []byte(file.content), 0600) + if err != nil { + t.Fatal(err) + } + + result, err := RetrieveVersion(filePath, conf) + if err != nil { + t.Fatal(err) + } + + if result != file.content { + t.Errorf("for file %s, expected %s but got %s", file.name, file.content, result) + } + } +} diff --git a/versionmanager/semantic/parser/iac/iacparser_test.go b/versionmanager/semantic/parser/iac/iacparser_test.go new file mode 100644 index 00000000..0ca2d32d --- /dev/null +++ b/versionmanager/semantic/parser/iac/iacparser_test.go @@ -0,0 +1,694 @@ +/* + * + * Copyright 2024 tofuutils authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package iacparser + +import ( + "os" + "path/filepath" + "sync" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/tofuutils/tenv/v4/config" +) + +// mockDisplayer implements loghelper.Displayer for testing +type mockDisplayer struct{} + +func (m *mockDisplayer) Display(string) {} +func (m *mockDisplayer) Log(hclog.Level, string, ...interface{}) {} +func (m *mockDisplayer) IsDebug() bool { return false } +func (m *mockDisplayer) IsTrace() bool { return false } +func (m *mockDisplayer) Flush(bool) {} + +// mockParser is a helper function that parses HCL content for testing +func mockParser(path string) (*hcl.File, hcl.Diagnostics) { + content, err := os.ReadFile(path) + if err != nil { + return nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Failed to read file", + Detail: err.Error(), + }} + } + + return hclsyntax.ParseConfig(content, path, hcl.Pos{Line: 1, Column: 1}) +} + +func TestGatherRequiredVersion(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + files map[string]string + exts []ExtDescription + expectedResult []string + expectError bool + }{ + { + name: "single file with version", + files: map[string]string{ + "main.tf": `terraform { + required_version = "~> 1.0.0" + }`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + }, + expectedResult: []string{"~> 1.0.0"}, + }, + { + name: "multiple files with versions", + files: map[string]string{ + "main.tf": `terraform { + required_version = "~> 1.0.0" + }`, + "other.tf": `terraform { + required_version = ">= 1.2.0" + }`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + }, + expectedResult: []string{"~> 1.0.0", ">= 1.2.0"}, + }, + { + name: "file without version", + files: map[string]string{ + "main.tf": `terraform { + backend "local" {} + }`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + }, + expectedResult: nil, + }, + { + name: "no extensions", + files: nil, + exts: nil, + expectedResult: nil, + }, + { + name: "invalid HCL", + files: map[string]string{ + "main.tf": `terraform { + required_version = + }`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + }, + expectError: true, + }, + { + name: "multiple file extensions", + files: map[string]string{ + "main.tf": `terraform { + required_version = "~> 1.0.0" + }`, + "other.hcl": `terraform { + required_version = ">= 1.2.0" + }`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + { + Value: ".hcl", + Parser: mockParser, + }, + }, + expectedResult: []string{"~> 1.0.0", ">= 1.2.0"}, + }, + { + name: "non-existent file", + files: map[string]string{ + "main.tf": `terraform { + required_version = "~> 1.0.0" + }`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: func(path string) (*hcl.File, hcl.Diagnostics) { + return nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "File not found", + }} + }, + }, + }, + expectError: true, + }, + { + name: "mixed valid and invalid files", + files: map[string]string{ + "main.tf": `terraform { + required_version = "~> 1.0.0" + }`, + "invalid.tf": `terraform { + required_version = + }`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + }, + expectError: true, + }, + { + name: "empty terraform block", + files: map[string]string{ + "main.tf": `terraform {}`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + }, + expectedResult: nil, + }, + { + name: "non-string version value", + files: map[string]string{ + "main.tf": `terraform { + required_version = 1.0 + }`, + }, + exts: []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Create test files + for name, content := range tt.files { + err := os.WriteFile(filepath.Join(tempDir, name), []byte(content), 0600) + if err != nil { + t.Fatal(err) + } + } + + // Create config + conf := &config.Config{ + WorkPath: tempDir, + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := GatherRequiredVersion(conf, tt.exts) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + + if len(result) != len(tt.expectedResult) { + t.Errorf("expected %d results but got %d", len(tt.expectedResult), len(result)) + } + + for i, v := range result { + if v != tt.expectedResult[i] { + t.Errorf("expected %s but got %s at index %d", tt.expectedResult[i], v, i) + } + } + }) + } +} + +func TestFilterExts(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + fileExts int + exts []ExtDescription + expectedResult ExtDescription + }{ + { + name: "single extension", + fileExts: 1, + exts: []ExtDescription{ + {Value: ".tf"}, + {Value: ".hcl"}, + }, + expectedResult: ExtDescription{Value: ".tf"}, + }, + { + name: "second extension", + fileExts: 2, + exts: []ExtDescription{ + {Value: ".tf"}, + {Value: ".hcl"}, + }, + expectedResult: ExtDescription{Value: ".hcl"}, + }, + { + name: "multiple bits set - first wins", + fileExts: 3, // 0b11 + exts: []ExtDescription{ + {Value: ".tf"}, + {Value: ".hcl"}, + }, + expectedResult: ExtDescription{Value: ".tf"}, + }, + { + name: "high bit set", + fileExts: 8, // 0b1000 + exts: []ExtDescription{ + {Value: ".tf"}, + {Value: ".hcl"}, + {Value: ".tfvars"}, + {Value: ".auto.tfvars"}, + }, + expectedResult: ExtDescription{Value: ".auto.tfvars"}, + }, + { + name: "all bits set", + fileExts: 15, // 0b1111 + exts: []ExtDescription{ + {Value: ".tf"}, + {Value: ".hcl"}, + {Value: ".tfvars"}, + {Value: ".auto.tfvars"}, + }, + expectedResult: ExtDescription{Value: ".tf"}, + }, + { + name: "single extension in list", + fileExts: 1, + exts: []ExtDescription{ + {Value: ".tf"}, + }, + expectedResult: ExtDescription{Value: ".tf"}, + }, + { + name: "no matching bits", + fileExts: 16, // 0b10000 + exts: []ExtDescription{ + {Value: ".tf"}, + {Value: ".hcl"}, + }, + expectedResult: ExtDescription{}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := filterExts(tt.fileExts, tt.exts) + if result.Value != tt.expectedResult.Value { + t.Errorf("expected %s but got %s", tt.expectedResult.Value, result.Value) + } + }) + } +} + +func TestExtractRequiredVersion(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + hclContent string + expectedResult []string + expectError bool + }{ + { + name: "valid required version", + hclContent: `terraform { + required_version = "~> 1.0.0" + }`, + expectedResult: []string{"~> 1.0.0"}, + }, + { + name: "multiple terraform blocks", + hclContent: `terraform { + required_version = "~> 1.0.0" + } + terraform { + required_version = ">= 1.2.0" + }`, + expectedResult: []string{"~> 1.0.0", ">= 1.2.0"}, + }, + { + name: "no required version", + hclContent: `terraform { + backend "local" {} + }`, + expectedResult: nil, + }, + { + name: "empty content", + hclContent: "", + expectedResult: nil, + }, + { + name: "complex terraform block", + hclContent: `terraform { + required_version = "~> 1.0.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" + } + } + backend "s3" { + bucket = "mybucket" + key = "path/to/my/key" + region = "us-east-1" + } + }`, + expectedResult: []string{"~> 1.0.0"}, + }, + { + name: "invalid version format", + hclContent: `terraform { + required_version = 123 + }`, + expectError: true, + }, + { + name: "multiple blocks with mixed content", + hclContent: `terraform { + required_version = "~> 1.0.0" + } + resource "aws_instance" "example" { + ami = "ami-123456" + } + terraform { + required_version = ">= 1.2.0" + backend "local" {} + }`, + expectedResult: []string{"~> 1.0.0", ">= 1.2.0"}, + }, + { + name: "terraform block with comments", + hclContent: `# Main terraform configuration + terraform { # Start block + # Version requirement + required_version = "~> 1.0.0" # Specify version constraint + } # End block`, + expectedResult: []string{"~> 1.0.0"}, + }, + { + name: "terraform block with heredoc", + hclContent: `terraform { + required_version = < 1.0.0 +EOF + }`, + expectedResult: []string{"~> 1.0.0"}, + }, + { + name: "invalid block structure", + hclContent: `terraform { + required_version = "~> 1.0.0" + { + invalid = true + } + }`, + expectError: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Parse HCL content + file, diags := hclsyntax.ParseConfig([]byte(tt.hclContent), "test.tf", hcl.Pos{Line: 1, Column: 1}) + if diags.HasErrors() { + t.Fatal(diags) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + result := extractRequiredVersion(file.Body, conf) + if len(result) != len(tt.expectedResult) { + t.Errorf("expected %d results but got %d", len(tt.expectedResult), len(result)) + } + + for i, v := range result { + if v != tt.expectedResult[i] { + t.Errorf("expected %s but got %s at index %d", tt.expectedResult[i], v, i) + } + } + }) + } +} + +func TestConcurrentAccess(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Create test files + files := map[string]string{ + "main.tf": `terraform { + required_version = "~> 1.0.0" + }`, + "other.tf": `terraform { + required_version = ">= 1.2.0" + }`, + } + + for name, content := range files { + err := os.WriteFile(filepath.Join(tempDir, name), []byte(content), 0600) + if err != nil { + t.Fatal(err) + } + } + + // Create config + conf := &config.Config{ + WorkPath: tempDir, + Displayer: &mockDisplayer{}, + } + + // Create extensions + exts := []ExtDescription{ + { + Value: ".tf", + Parser: mockParser, + }, + } + + // Number of concurrent goroutines + numGoroutines := 10 + var wg sync.WaitGroup + wg.Add(numGoroutines) + + // Run concurrent tests + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + result, err := GatherRequiredVersion(conf, exts) + if err != nil { + t.Error(err) + return + } + if len(result) != 2 { + t.Errorf("expected 2 results but got %d", len(result)) + } + }() + } + + wg.Wait() +} + +func TestFileErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(string) error + expectError bool + }{ + { + name: "unreadable directory", + setup: func(dir string) error { + return os.Chmod(dir, 0000) + }, + expectError: true, + }, + { + name: "unreadable file", + setup: func(dir string) error { + filePath := filepath.Join(dir, "main.tf") + if err := os.WriteFile(filePath, []byte(`terraform {}`), 0600); err != nil { + return err + } + return os.Chmod(filePath, 0000) + }, + expectError: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Apply setup + if err := tt.setup(tempDir); err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + WorkPath: tempDir, + Displayer: &mockDisplayer{}, + } + + // Run test + _, err := GatherRequiredVersion(conf, []ExtDescription{{ + Value: ".tf", + Parser: mockParser, + }}) + + if tt.expectError && err == nil { + t.Error("expected error but got nil") + } + }) + } +} + +func TestParserErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + parser func(string) (*hcl.File, hcl.Diagnostics) + expectError bool + }{ + { + name: "nil file no error", + parser: func(string) (*hcl.File, hcl.Diagnostics) { + return nil, nil + }, + expectError: false, + }, + { + name: "nil file with error", + parser: func(string) (*hcl.File, hcl.Diagnostics) { + return nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: "Test error", + }} + }, + expectError: true, + }, + { + name: "parser panic", + parser: func(string) (*hcl.File, hcl.Diagnostics) { + panic("parser error") + }, + expectError: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "main.tf") + if err := os.WriteFile(filePath, []byte(`terraform {}`), 0600); err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + WorkPath: tempDir, + Displayer: &mockDisplayer{}, + } + + // Run test with recovery for panics + var err error + func() { + defer func() { + if r := recover(); r != nil && !tt.expectError { + t.Errorf("unexpected panic: %v", r) + } + }() + + _, err = GatherRequiredVersion(conf, []ExtDescription{{ + Value: ".tf", + Parser: tt.parser, + }}) + }() + + if tt.expectError && err == nil { + t.Error("expected error but got nil") + } + }) + } +} diff --git a/versionmanager/semantic/parser/toml/tomlparser_test.go b/versionmanager/semantic/parser/toml/tomlparser_test.go new file mode 100644 index 00000000..2b84a735 --- /dev/null +++ b/versionmanager/semantic/parser/toml/tomlparser_test.go @@ -0,0 +1,545 @@ +/* + * + * Copyright 2024 tofuutils authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package tomlparser + +import ( + "os" + "path/filepath" + "sync" + "testing" + + "github.com/hashicorp/go-hclog" + + "github.com/tofuutils/tenv/v4/config" +) + +// mockDisplayer implements loghelper.Displayer for testing +type mockDisplayer struct{} + +func (m *mockDisplayer) Display(string) {} +func (m *mockDisplayer) Log(hclog.Level, string, ...interface{}) {} +func (m *mockDisplayer) IsDebug() bool { return false } +func (m *mockDisplayer) IsTrace() bool { return false } +func (m *mockDisplayer) Flush(bool) {} + +func TestRetrieveVersion(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + expectedResult string + expectError bool + }{ + { + name: "valid version constraint", + content: `[tool.terraform] + required_version = "~> 1.0.0"`, + expectedResult: "~> 1.0.0", + }, + { + name: "no version constraint", + content: `[tool.terraform] + backend = "local"`, + expectedResult: "", + }, + { + name: "invalid TOML", + content: `[tool.terraform + required_version = "~> 1.0.0"`, + expectError: true, + }, + { + name: "non-string version constraint", + content: `[tool.terraform] + required_version = 1.0`, + expectError: true, + }, + { + name: "empty version constraint", + content: `[tool.terraform] + required_version = ""`, + expectedResult: "", + }, + { + name: "complex TOML with version constraint", + content: `[tool.terraform] + required_version = "~> 1.0.0" + [tool.terraform.required_providers] + aws = { source = "hashicorp/aws", version = "~> 3.0" }`, + expectedResult: "~> 1.0.0", + }, + { + name: "multiple version constraints", + content: `[tool.terraform] + required_version = ">= 1.0.0, < 2.0.0"`, + expectedResult: ">= 1.0.0, < 2.0.0", + }, + { + name: "nested tables", + content: `[tool] + [tool.terraform] + required_version = "~> 1.0.0"`, + expectedResult: "~> 1.0.0", + }, + { + name: "array of tables", + content: `[[tool.terraform]] + required_version = "~> 1.0.0"`, + expectedResult: "~> 1.0.0", + }, + { + name: "inline tables", + content: `[tool.terraform] + required_version = "~> 1.0.0" + backend = { type = "local" }`, + expectedResult: "~> 1.0.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "pyproject.toml") + + // Create test file + err := os.WriteFile(filePath, []byte(tt.content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + + if result != tt.expectedResult { + t.Errorf("expected %s but got %s", tt.expectedResult, result) + } + }) + } +} + +func TestConcurrentAccess(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "pyproject.toml") + + // Create test file + content := `[tool.terraform] + required_version = "~> 1.0.0"` + err := os.WriteFile(filePath, []byte(content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Number of concurrent goroutines + numGoroutines := 10 + var wg sync.WaitGroup + wg.Add(numGoroutines) + + // Run concurrent tests + for i := 0; i < numGoroutines; i++ { + go func() { + defer wg.Done() + result, err := RetrieveVersion(filePath, conf) + if err != nil { + t.Error(err) + return + } + if result != "~> 1.0.0" { + t.Errorf("expected ~> 1.0.0 but got %s", result) + } + }() + } + + wg.Wait() +} + +func TestFileErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(string) error + expectError bool + }{ + { + name: "non-existent file", + setup: func(dir string) error { + return nil // No setup needed, file doesn't exist + }, + expectError: false, // Should return empty string, not error + }, + { + name: "unreadable file", + setup: func(dir string) error { + filePath := filepath.Join(dir, "pyproject.toml") + if err := os.WriteFile(filePath, []byte(`[tool.terraform]`), 0600); err != nil { + return err + } + return os.Chmod(filePath, 0000) + }, + expectError: true, + }, + { + name: "directory instead of file", + setup: func(dir string) error { + filePath := filepath.Join(dir, "pyproject.toml") + return os.Mkdir(filePath, 0700) + }, + expectError: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Apply setup + if err := tt.setup(tempDir); err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + filePath := filepath.Join(tempDir, "pyproject.toml") + _, err := RetrieveVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + }) + } +} + +func TestParserErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + expectError bool + }{ + { + name: "empty content", + content: "", + expectError: false, + }, + { + name: "invalid TOML syntax", + content: `[tool.terraform + required_version = "~> 1.0.0"`, + expectError: true, + }, + { + name: "invalid version format", + content: `[tool.terraform] + required_version = 1.0`, + expectError: true, + }, + { + name: "missing tool section", + content: `[terraform] + required_version = "~> 1.0.0"`, + expectError: false, + }, + { + name: "missing terraform section", + content: `[tool] + required_version = "~> 1.0.0"`, + expectError: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "pyproject.toml") + + // Create test file + err := os.WriteFile(filePath, []byte(tt.content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + _, err = RetrieveVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + }) + } +} + +func TestFileEncodings(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content []byte + expectedResult string + expectError bool + }{ + { + name: "UTF-8", + content: []byte(`[tool.terraform]\nrequired_version = "~> 1.0.0"`), + expectedResult: "~> 1.0.0", + }, + { + name: "UTF-8 with BOM", + content: append([]byte{0xEF, 0xBB, 0xBF}, []byte(`[tool.terraform]\nrequired_version = "~> 1.0.0"`)...), + expectedResult: "~> 1.0.0", + }, + { + name: "UTF-16", + content: append([]byte{0xFF, 0xFE}, []byte(`[tool.terraform]\nrequired_version = "~> 1.0.0"`)...), + expectError: true, + }, + { + name: "ASCII", + content: []byte(`[tool.terraform]\nrequired_version = "~> 1.0.0"`), + expectedResult: "~> 1.0.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "pyproject.toml") + + // Create test file + err := os.WriteFile(filePath, tt.content, 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveVersion(filePath, conf) + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + } + return + } + + if err != nil { + t.Fatal(err) + } + + if result != tt.expectedResult { + t.Errorf("expected %s but got %s", tt.expectedResult, result) + } + }) + } +} + +func TestLargeFiles(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + filePath := filepath.Join(tempDir, "pyproject.toml") + + // Create a large file with version constraint + content := make([]byte, 10*1024*1024) // 10MB + copy(content, []byte(`[tool.terraform]\nrequired_version = "~> 1.0.0"`)) + + // Create test file + err := os.WriteFile(filePath, content, 0600) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveVersion(filePath, conf) + if err != nil { + t.Fatal(err) + } + + if result != "~> 1.0.0" { + t.Errorf("expected ~> 1.0.0 but got %s", result) + } +} + +func TestSymbolicLinks(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + originalPath := filepath.Join(tempDir, "original.toml") + linkPath := filepath.Join(tempDir, "pyproject.toml") + + // Create original file + content := `[tool.terraform]\nrequired_version = "~> 1.0.0"` + err := os.WriteFile(originalPath, []byte(content), 0600) + if err != nil { + t.Fatal(err) + } + + // Create symbolic link + err = os.Symlink(originalPath, linkPath) + if err != nil { + t.Fatal(err) + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Run test + result, err := RetrieveVersion(linkPath, conf) + if err != nil { + t.Fatal(err) + } + + if result != "~> 1.0.0" { + t.Errorf("expected ~> 1.0.0 but got %s", result) + } +} + +func TestMultipleFiles(t *testing.T) { + t.Parallel() + + // Setup temp directory + tempDir := t.TempDir() + + // Create multiple files with different version constraints + files := []struct { + name string + content string + }{ + { + name: "pyproject.toml", + content: `[tool.terraform]\nrequired_version = "~> 1.0.0"`, + }, + { + name: "poetry.toml", + content: `[tool.terraform]\nrequired_version = "~> 1.1.0"`, + }, + { + name: "config.toml", + content: `[tool.terraform]\nbackend = "local"`, + }, + } + + // Create config + conf := &config.Config{ + Displayer: &mockDisplayer{}, + } + + // Create and test each file + for _, file := range files { + filePath := filepath.Join(tempDir, file.name) + err := os.WriteFile(filePath, []byte(file.content), 0600) + if err != nil { + t.Fatal(err) + } + + result, err := RetrieveVersion(filePath, conf) + if err != nil { + t.Fatal(err) + } + + expected := "" + if file.name != "config.toml" { + expected = "~> 1.0.0" + if file.name == "poetry.toml" { + expected = "~> 1.1.0" + } + } + + if result != expected { + t.Errorf("for file %s, expected %s but got %s", file.name, expected, result) + } + } +} diff --git a/versionmanager/semantic/semantic_test.go b/versionmanager/semantic/semantic_test.go index d820af0f..b1f90968 100644 --- a/versionmanager/semantic/semantic_test.go +++ b/versionmanager/semantic/semantic_test.go @@ -19,12 +19,77 @@ package semantic_test import ( + "context" + "os" + "path/filepath" "slices" + "strconv" "testing" + "time" + "github.com/hashicorp/go-version" + "github.com/tofuutils/tenv/v4/config" + "github.com/tofuutils/tenv/v4/versionmanager/lastuse" "github.com/tofuutils/tenv/v4/versionmanager/semantic" + "github.com/tofuutils/tenv/v4/versionmanager/semantic/types" ) +const ( + allKey = "all" + butLast = "butlast" +) + +type mockConstraintInfo struct { + constraint string +} + +func (m *mockConstraintInfo) ReadDefaultConstraint() string { + return m.constraint +} + +type mockVersionManager struct { + listVersionsFunc func() ([]string, error) +} + +func (m *mockVersionManager) ListRemote(ctx context.Context, reverseOrder bool) ([]string, error) { + if m.listVersionsFunc != nil { + return m.listVersionsFunc() + } + return nil, nil +} + +func (m *mockVersionManager) Detect(ctx context.Context, folderName string, conf *config.Config) (string, error) { + return "", nil +} + +func (m *mockVersionManager) Evaluate(ctx context.Context, versionStr string, folderName string, conf *config.Config) (string, error) { + return "", nil +} + +func (m *mockVersionManager) Install(ctx context.Context, versionStr string, conf *config.Config) error { + return nil +} + +func (m *mockVersionManager) Uninstall(ctx context.Context, versionStr string, conf *config.Config) error { + return nil +} + +func (m *mockVersionManager) ListLocal(ctx context.Context) ([]string, error) { + return nil, nil +} + +func (m *mockVersionManager) Use(ctx context.Context, versionStr string, conf *config.Config) error { + return nil +} + +func (m *mockVersionManager) GetLastUse(ctx context.Context) (*lastuse.LastUse, error) { + return nil, nil +} + +func (m *mockVersionManager) WriteLastUse(ctx context.Context, lastUse *lastuse.LastUse) error { + return nil +} + func TestCmpVersion(t *testing.T) { t.Parallel() @@ -39,13 +104,384 @@ func TestStableVersion(t *testing.T) { t.Parallel() var filtered []string - for _, version := range []string{"1.5.0", "1.5.1", "1.5.2", "1.6.0-alpha5", "1.6.0-beta5", "1.6.0-rc1", "1.6.0"} { - if semantic.StableVersion(version) { - filtered = append(filtered, version) + for _, v := range []string{"1.5.0", "1.5.1", "1.5.2", "1.6.0-alpha5", "1.6.0-beta5", "1.6.0-rc1", "1.6.0"} { + if semantic.StableVersion(v) { + filtered = append(filtered, v) } } - if !slices.Equal(filtered, []string{"1.5.0", "1.5.1", "1.5.2", "1.6.0"}) { t.Error("Unmatching results, get :", filtered) } } + +func TestParsePredicate(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + versionStr string + folderName string + vm types.ConstraintInfo + conf *config.Config + want types.PredicateInfo + wantErr bool + }{ + { + name: "valid version", + versionStr: "1.0.0", + folderName: "test", + vm: &mockConstraintInfo{}, + conf: &config.Config{}, + want: types.PredicateInfo{ + Predicate: func(v string) bool { + ver, err := version.NewVersion(v) + return err == nil && ver.String() == "1.0.0" + }, + ReverseOrder: false, + }, + wantErr: false, + }, + { + name: "invalid version", + versionStr: "invalid", + folderName: "test", + vm: &mockConstraintInfo{}, + conf: &config.Config{}, + want: types.PredicateInfo{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := semantic.ParsePredicate(tt.versionStr, tt.folderName, tt.vm, nil, tt.conf) + if (err != nil) != tt.wantErr { + t.Errorf("ParsePredicate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil && !got.Predicate("1.0.0") { + t.Error("ParsePredicate() = false, want true") + } + }) + } +} + +func TestAddDefaultConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want string + }{ + { + name: "empty constraint", + constraint: "", + want: ">= 0.0.0", + }, + { + name: "existing constraint", + constraint: ">= 1.0.0", + want: ">= 1.0.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + info := &mockConstraintInfo{constraint: tt.constraint} + if got := semantic.AddDefaultConstraint(info); got != tt.want { + t.Errorf("AddDefaultConstraint() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPredicateFromConstraint(t *testing.T) { + tests := []struct { + name string + constraint string + want types.PredicateInfo + wantErr bool + }{ + { + name: "valid constraint", + constraint: ">= 1.0.0", + want: types.PredicateInfo{ + Predicate: func(v string) bool { + ver, _ := version.NewVersion(v) + return ver.Compare(version.Must(version.NewVersion("1.0.0"))) >= 0 + }, + ReverseOrder: false, + }, + wantErr: false, + }, + { + name: "invalid constraint", + constraint: "invalid", + want: types.PredicateInfo{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := semantic.PredicateFromConstraint(tt.constraint) + if (err != nil) != tt.wantErr { + t.Errorf("PredicateFromConstraint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + // Test with a sample version + testVer := "1.1.0" + if got.Predicate(testVer) != tt.want.Predicate(testVer) { + t.Errorf("PredicateFromConstraint() = %v, want %v", got, tt.want) + } + } + }) + } +} + +func mustNewConstraint(t *testing.T, constraint string) version.Constraints { + t.Helper() + c, err := version.NewConstraint(constraint) + if err != nil { + t.Fatalf("Failed to create constraint: %v", err) + } + return c +} + +func TestSelectVersionsToUninstall(t *testing.T) { + t.Parallel() + + testVersions := []string{"1.6.0", "1.5.2", "1.5.1", "1.5.0"} + testPath := "/test/path" + testConfig := &config.Config{} + + tests := []struct { + name string + behaviour string + versions []string + want []string + wantErr bool + }{ + { + name: "all versions", + behaviour: allKey, + versions: testVersions, + want: testVersions, + wantErr: false, + }, + { + name: "but last version", + behaviour: butLast, + versions: testVersions, + want: []string{"1.5.2", "1.5.1", "1.5.0"}, + wantErr: false, + }, + { + name: "but last with empty list", + behaviour: butLast, + versions: []string{}, + want: nil, + wantErr: false, + }, + { + name: "version constraint", + behaviour: "< 1.5.2", + versions: testVersions, + want: []string{"1.5.1", "1.5.0"}, + wantErr: false, + }, + { + name: "invalid version constraint", + behaviour: "invalid", + versions: testVersions, + want: nil, + wantErr: true, + }, + { + name: "not used for days - invalid format", + behaviour: "not-used-for:abc", + versions: testVersions, + want: nil, + wantErr: true, + }, + { + name: "not used for days - valid format", + behaviour: "not-used-for:30d", + versions: testVersions, + want: []string{}, // Empty because no files exist in test path + wantErr: false, + }, + { + name: "not used for months - valid format", + behaviour: "not-used-for:2m", + versions: testVersions, + want: []string{}, // Empty because no files exist in test path + wantErr: false, + }, + { + name: "not used since - invalid date", + behaviour: "not-used-since:invalid", + versions: testVersions, + want: nil, + wantErr: true, + }, + { + name: "not used since - valid date", + behaviour: "not-used-since:2024-01-01", + versions: testVersions, + want: []string{}, // Empty because no files exist in test path + wantErr: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := semantic.SelectVersionsToUninstall(tt.behaviour, testPath, tt.versions, testConfig) + if (err != nil) != tt.wantErr { + t.Errorf("SelectVersionsToUninstall() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !slices.Equal(got, tt.want) { + t.Errorf("SelectVersionsToUninstall() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFilterStrings(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []string + pred func(string) bool + expected []string + }{ + { + name: "filter even numbers", + input: []string{"1", "2", "3", "4", "5"}, + pred: func(s string) bool { n, _ := strconv.Atoi(s); return n%2 == 0 }, + expected: []string{"2", "4"}, + }, + { + name: "empty input", + input: []string{}, + pred: func(s string) bool { return true }, + expected: []string{}, + }, + { + name: "no matches", + input: []string{"1", "3", "5"}, + pred: func(s string) bool { n, _ := strconv.Atoi(s); return n%2 == 0 }, + expected: []string{}, + }, + { + name: "all matches", + input: []string{"2", "4", "6"}, + pred: func(s string) bool { n, _ := strconv.Atoi(s); return n%2 == 0 }, + expected: []string{"2", "4", "6"}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := filterStrings(tt.input, tt.pred) + if !slices.Equal(result, tt.expected) { + t.Errorf("filterStrings() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestPredicateBeforeDate(t *testing.T) { + t.Parallel() + + testPath := t.TempDir() + testVersion := "1.0.0" + versionPath := filepath.Join(testPath, testVersion) + if err := os.MkdirAll(versionPath, 0755); err != nil { + t.Fatal(err) + } + + // Create a last use file with a known date + testDate := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + lastuse.Write(versionPath, testDate, &config.Config{}) + + tests := []struct { + name string + beforeDate time.Time + want bool + }{ + { + name: "date before last use", + beforeDate: time.Date(2023, 12, 31, 0, 0, 0, 0, time.UTC), + want: false, + }, + { + name: "date after last use", + beforeDate: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC), + want: true, + }, + { + name: "same date as last use", + beforeDate: testDate, + want: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + pred := predicateBeforeDate(testPath, tt.beforeDate, &config.Config{}) + if got := pred(testVersion); got != tt.want { + t.Errorf("predicateBeforeDate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseVersion(t *testing.T) { + tests := []struct { + name string + version string + want *version.Version + wantErr bool + }{ + { + name: "valid version", + version: "1.0.0", + want: version.Must(version.NewVersion("1.0.0")), + wantErr: false, + }, + { + name: "invalid version", + version: "invalid", + want: nil, + wantErr: true, + }, + { + name: "empty version", + version: "", + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseVersion(tt.version) + if (err != nil) != tt.wantErr { + t.Errorf("ParseVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err == nil && got.String() != tt.want.String() { + t.Errorf("ParseVersion() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/versionmanager/semantic/types/types_test.go b/versionmanager/semantic/types/types_test.go new file mode 100644 index 00000000..65d3745b --- /dev/null +++ b/versionmanager/semantic/types/types_test.go @@ -0,0 +1,336 @@ +/* + * + * Copyright 2024 tofuutils authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package types + +import ( + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/tofuutils/tenv/v4/config" +) + +// mockDisplayer implements loghelper.Displayer for testing +type mockDisplayer struct { + displayedMessages []string +} + +func (m *mockDisplayer) Display(msg string) { + m.displayedMessages = append(m.displayedMessages, msg) +} + +func (m *mockDisplayer) Log(hclog.Level, string, ...interface{}) {} +func (m *mockDisplayer) IsDebug() bool { return false } +func (m *mockDisplayer) IsTrace() bool { return false } +func (m *mockDisplayer) Flush(bool) {} + +func TestDisplayDetectionInfo(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + version string + source string + expectedMsg string + expectedResult string + }{ + { + name: "valid version and source", + version: "1.0.0", + source: "test.txt", + expectedMsg: "Resolved version from test.txt : 1.0.0", + expectedResult: "1.0.0", + }, + { + name: "empty version", + version: "", + source: "test.txt", + expectedMsg: "Resolved version from test.txt : ", + expectedResult: "", + }, + { + name: "empty source", + version: "1.0.0", + source: "", + expectedMsg: "Resolved version from : 1.0.0", + expectedResult: "1.0.0", + }, + { + name: "both empty", + version: "", + source: "", + expectedMsg: "Resolved version from : ", + expectedResult: "", + }, + { + name: "version with spaces", + version: " 1.0.0 ", + source: "test.txt", + expectedMsg: "Resolved version from test.txt : 1.0.0 ", + expectedResult: " 1.0.0 ", + }, + { + name: "source with spaces", + version: "1.0.0", + source: " test.txt ", + expectedMsg: "Resolved version from test.txt : 1.0.0", + expectedResult: "1.0.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create mock displayer + displayer := &mockDisplayer{} + + // Run test + result := DisplayDetectionInfo(displayer, tt.version, tt.source) + + // Check result + if result != tt.expectedResult { + t.Errorf("expected result %s but got %s", tt.expectedResult, result) + } + + // Check displayed message + if len(displayer.displayedMessages) != 1 { + t.Errorf("expected 1 displayed message but got %d", len(displayer.displayedMessages)) + } else if displayer.displayedMessages[0] != tt.expectedMsg { + t.Errorf("expected message %s but got %s", tt.expectedMsg, displayer.displayedMessages[0]) + } + }) + } +} + +func TestPredicateInfo(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + predicate func(string) bool + reverseOrder bool + testValue string + expectedValue bool + }{ + { + name: "always true predicate", + predicate: func(s string) bool { + return true + }, + reverseOrder: false, + testValue: "test", + expectedValue: true, + }, + { + name: "always false predicate", + predicate: func(s string) bool { + return false + }, + reverseOrder: false, + testValue: "test", + expectedValue: false, + }, + { + name: "length check predicate", + predicate: func(s string) bool { + return len(s) > 3 + }, + reverseOrder: false, + testValue: "test", + expectedValue: true, + }, + { + name: "length check predicate with short string", + predicate: func(s string) bool { + return len(s) > 3 + }, + reverseOrder: false, + testValue: "hi", + expectedValue: false, + }, + { + name: "reverse order with true predicate", + predicate: func(s string) bool { + return true + }, + reverseOrder: true, + testValue: "test", + expectedValue: true, + }, + { + name: "reverse order with false predicate", + predicate: func(s string) bool { + return false + }, + reverseOrder: true, + testValue: "test", + expectedValue: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create PredicateInfo + info := PredicateInfo{ + Predicate: tt.predicate, + ReverseOrder: tt.reverseOrder, + } + + // Run test + result := info.Predicate(tt.testValue) + + // Check result + if result != tt.expectedValue { + t.Errorf("expected %v but got %v", tt.expectedValue, result) + } + }) + } +} + +func TestVersionFile(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + fileName string + parser func(string, *config.Config) (string, error) + expectedName string + expectedParser bool + }{ + { + name: "valid file with parser", + fileName: "test.txt", + parser: func(s string, c *config.Config) (string, error) { + return "1.0.0", nil + }, + expectedName: "test.txt", + expectedParser: true, + }, + { + name: "valid file without parser", + fileName: "test.txt", + parser: nil, + expectedName: "test.txt", + expectedParser: false, + }, + { + name: "empty file name", + fileName: "", + parser: nil, + expectedName: "", + expectedParser: false, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create VersionFile + file := VersionFile{ + Name: tt.fileName, + Parser: tt.parser, + } + + // Check name + if file.Name != tt.expectedName { + t.Errorf("expected name %s but got %s", tt.expectedName, file.Name) + } + + // Check parser + if (file.Parser != nil) != tt.expectedParser { + t.Errorf("expected parser %v but got %v", tt.expectedParser, file.Parser != nil) + } + + // If parser exists, test it + if file.Parser != nil { + result, err := file.Parser("test.txt", nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if result != "1.0.0" { + t.Errorf("expected result 1.0.0 but got %s", result) + } + } + }) + } +} + +// Mock implementation of ConstraintInfo +type mockConstraintInfo struct { + constraint string +} + +var _ ConstraintInfo = (*mockConstraintInfo)(nil) // Ensure mock implements the interface + +// Method implementation +func (m *mockConstraintInfo) ReadDefaultConstraint() string { + return m.constraint +} + +func TestConstraintInfo(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + constraint string + expectedResult string + }{ + { + name: "valid constraint", + constraint: ">= 1.0.0", + expectedResult: ">= 1.0.0", + }, + { + name: "empty constraint", + constraint: "", + expectedResult: "", + }, + { + name: "constraint with spaces", + constraint: " >= 1.0.0 ", + expectedResult: " >= 1.0.0 ", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Create mock + mock := &mockConstraintInfo{ + constraint: tt.constraint, + } + + // Run test + result := mock.ReadDefaultConstraint() + + // Check result + if result != tt.expectedResult { + t.Errorf("expected result %s but got %s", tt.expectedResult, result) + } + }) + } +}