package config import ( "os" "path/filepath" "strings" "testing" ) func TestLoadSecretEnvironmentLoadsValidFiles(t *testing.T) { directory := t.TempDir() writeSecret(t, directory, "API_KEY", "value\n") writeSecret(t, directory, "CRLF", "value\r\n") writeSecret(t, directory, "MULTILINE", "value\n\n") writeSecret(t, directory, "SPACES", " value \n") writeSecret(t, directory, "CARRIAGE", "value\r") result, err := LoadSecretEnvironment(directory, emptyLookup) if err != nil { t.Fatalf("LoadSecretEnvironment() error = %v", err) } assertEnvValue(t, result.Environment, "API_KEY", "value") assertEnvValue(t, result.Environment, "CRLF", "value") assertEnvValue(t, result.Environment, "MULTILINE", "value\n") assertEnvValue(t, result.Environment, "SPACES", " value ") assertEnvValue(t, result.Environment, "CARRIAGE", "value\r") } func TestLoadSecretEnvironmentRejectsInvalidFilenames(t *testing.T) { directory := t.TempDir() secretValue := "do-not-print" writeSecret(t, directory, "1INVALID", secretValue) _, err := LoadSecretEnvironment(directory, emptyLookup) if err == nil { t.Fatal("LoadSecretEnvironment() error = nil, want error") } if !strings.Contains(err.Error(), "secret filename") { t.Fatalf("LoadSecretEnvironment() error = %q, want filename error", err) } if strings.Contains(err.Error(), secretValue) { t.Fatalf("LoadSecretEnvironment() error exposed secret value: %q", err) } } func TestLoadSecretEnvironmentIgnoresDirectoriesAndFollowsSymlinks(t *testing.T) { directory := t.TempDir() if err := os.Mkdir(filepath.Join(directory, "IGNORED_DIR"), 0o700); err != nil { t.Fatalf("mkdir ignored dir: %v", err) } targetFile := filepath.Join(t.TempDir(), "target") if err := os.WriteFile(targetFile, []byte("linked\n"), 0o600); err != nil { t.Fatalf("write target file: %v", err) } if err := os.Symlink(targetFile, filepath.Join(directory, "LINKED_SECRET")); err != nil { t.Fatalf("symlink file: %v", err) } targetDir := t.TempDir() if err := os.Symlink(targetDir, filepath.Join(directory, "LINKED_DIR")); err != nil { t.Fatalf("symlink dir: %v", err) } result, err := LoadSecretEnvironment(directory, emptyLookup) if err != nil { t.Fatalf("LoadSecretEnvironment() error = %v", err) } assertEnvValue(t, result.Environment, "LINKED_SECRET", "linked") if _, ok := result.Environment.Lookup("IGNORED_DIR"); ok { t.Fatal("directory appeared in environment") } if _, ok := result.Environment.Lookup("LINKED_DIR"); ok { t.Fatal("directory symlink appeared in environment") } } func TestLoadSecretEnvironmentMissingDirectoryFails(t *testing.T) { missing := filepath.Join(t.TempDir(), "missing") _, err := LoadSecretEnvironment(missing, emptyLookup) if err == nil { t.Fatal("LoadSecretEnvironment() error = nil, want error") } if !strings.Contains(err.Error(), "load secrets directory") || !strings.Contains(err.Error(), missing) { t.Fatalf("LoadSecretEnvironment() error = %q, want directory context", err) } } func TestEnvironmentPrefersProcessValuesAndReportsDifferingConflicts(t *testing.T) { directory := t.TempDir() secretValue := "secret-value" processValue := "process-value" writeSecret(t, directory, "TOKEN", secretValue) result, err := LoadSecretEnvironment(directory, mapLookup(map[string]string{"TOKEN": processValue})) if err != nil { t.Fatalf("LoadSecretEnvironment() error = %v", err) } assertEnvValue(t, result.Environment, "TOKEN", processValue) if got, want := len(result.Conflicts), 1; got != want { t.Fatalf("conflict count = %d, want %d", got, want) } if result.Conflicts[0].Name != "TOKEN" { t.Fatalf("conflict name = %q, want TOKEN", result.Conflicts[0].Name) } if strings.Contains(result.Conflicts[0].Name, secretValue) || strings.Contains(result.Conflicts[0].Name, processValue) { t.Fatalf("conflict exposed secret values: %#v", result.Conflicts[0]) } } func TestEnvironmentDoesNotWarnWhenProcessValueMatchesSecret(t *testing.T) { directory := t.TempDir() writeSecret(t, directory, "TOKEN", "same-value") result, err := LoadSecretEnvironment(directory, mapLookup(map[string]string{"TOKEN": "same-value"})) if err != nil { t.Fatalf("LoadSecretEnvironment() error = %v", err) } if len(result.Conflicts) != 0 { t.Fatalf("conflicts = %#v, want none", result.Conflicts) } } func TestEnvironmentDoesNotMutateProcessEnvironment(t *testing.T) { name := "DISTRIBUTOR_TEST_SECRET_ONLY" t.Setenv(name, "") if err := os.Unsetenv(name); err != nil { t.Fatalf("unset env: %v", err) } directory := t.TempDir() writeSecret(t, directory, name, "secret") if _, err := LoadSecretEnvironment(directory, nil); err != nil { t.Fatalf("LoadSecretEnvironment() error = %v", err) } if _, ok := os.LookupEnv(name); ok { t.Fatalf("%s was added to process environment", name) } } func TestResolveCredentialsUsesSecretsAwareEnvironment(t *testing.T) { directory := t.TempDir() writeSecret(t, directory, "ACCESS_KEY_ID", "secret-access") writeSecret(t, directory, "SECRET_ACCESS_KEY", "secret-secret") result, err := LoadSecretEnvironment(directory, emptyLookup) if err != nil { t.Fatalf("LoadSecretEnvironment() error = %v", err) } creds, err := result.Environment.ResolveCredentials(Credentials{ AccessKeyIDEnv: "ACCESS_KEY_ID", SecretAccessKeyEnv: "SECRET_ACCESS_KEY", }) if err != nil { t.Fatalf("ResolveCredentials() error = %v", err) } if creds.AccessKeyID != "secret-access" || creds.SecretAccessKey != "secret-secret" { t.Fatalf("resolved credentials = %#v", creds) } } func TestResolveCredentialsPrefersProcessEnvironment(t *testing.T) { directory := t.TempDir() writeSecret(t, directory, "ACCESS_KEY_ID", "secret-access") result, err := LoadSecretEnvironment(directory, mapLookup(map[string]string{"ACCESS_KEY_ID": "process-access"})) if err != nil { t.Fatalf("LoadSecretEnvironment() error = %v", err) } creds, err := result.Environment.ResolveCredentials(Credentials{AccessKeyIDEnv: "ACCESS_KEY_ID"}) if err != nil { t.Fatalf("ResolveCredentials() error = %v", err) } if creds.AccessKeyID != "process-access" { t.Fatalf("access key = %q, want process-access", creds.AccessKeyID) } } func TestResolveCredentialsDoesNotFeedProcessEnvironment(t *testing.T) { name := "DISTRIBUTOR_TEST_SDK_CHAIN_VALUE" t.Setenv(name, "") if err := os.Unsetenv(name); err != nil { t.Fatalf("unset env: %v", err) } directory := t.TempDir() writeSecret(t, directory, name, "secret") result, err := LoadSecretEnvironment(directory, nil) if err != nil { t.Fatalf("LoadSecretEnvironment() error = %v", err) } assertEnvValue(t, result.Environment, name, "secret") if _, ok := os.LookupEnv(name); ok { t.Fatalf("%s is visible to process environment", name) } } func TestResolveCredentialsMissingReferenceFailsWithoutSecretValue(t *testing.T) { env := NewEnvironment(map[string]string{"PRESENT": "do-not-print"}, emptyLookup) _, err := env.ResolveCredentials(Credentials{AccessKeyIDEnv: "MISSING"}) if err == nil { t.Fatal("ResolveCredentials() error = nil, want error") } if !strings.Contains(err.Error(), "MISSING") { t.Fatalf("ResolveCredentials() error = %q, want missing variable name", err) } if strings.Contains(err.Error(), "do-not-print") { t.Fatalf("ResolveCredentials() error exposed secret value: %q", err) } } func TestResolveCredentialsRejectsEmptyReferencedValue(t *testing.T) { env := NewEnvironment(map[string]string{"EMPTY": ""}, emptyLookup) _, err := env.ResolveCredentials(Credentials{AccessKeyIDEnv: "EMPTY"}) if err == nil { t.Fatal("ResolveCredentials() error = nil, want error") } if !strings.Contains(err.Error(), "EMPTY") || !strings.Contains(err.Error(), "empty") { t.Fatalf("ResolveCredentials() error = %q, want empty variable context", err) } } func writeSecret(t *testing.T, directory, name, value string) { t.Helper() if err := os.WriteFile(filepath.Join(directory, name), []byte(value), 0o600); err != nil { t.Fatalf("write secret %s: %v", name, err) } } func assertEnvValue(t *testing.T, env Environment, name, want string) { t.Helper() got, ok := env.Lookup(name) if !ok { t.Fatalf("Lookup(%q) ok = false", name) } if got != want { t.Fatalf("Lookup(%q) = %q, want %q", name, got, want) } } func emptyLookup(string) (string, bool) { return "", false } func mapLookup(values map[string]string) EnvLookup { return func(name string) (string, bool) { value, ok := values[name] return value, ok } }