package secrets import ( "os" "path/filepath" "strings" "sync" "testing" "github.com/wild-cloud/wild-central/internal/storage" ) func TestGenerateSecret(t *testing.T) { tests := []struct { name string length int want int }{ {"default length", DefaultSecretLength, DefaultSecretLength}, {"custom length 64", 64, 64}, {"custom length 128", 128, 128}, {"zero defaults", 0, DefaultSecretLength}, {"negative defaults", -1, DefaultSecretLength}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { secret, err := GenerateSecret(tt.length) if err != nil { t.Fatalf("GenerateSecret failed: %v", err) } if len(secret) != tt.want { t.Errorf("got length %d, want %d", len(secret), tt.want) } for _, c := range secret { if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) { t.Errorf("non-alphanumeric character found: %c", c) } } }) } } func TestGenerateSecret_Uniqueness(t *testing.T) { const numSecrets = 100 seen := make(map[string]bool, numSecrets) for i := 0; i < numSecrets; i++ { secret, err := GenerateSecret(32) if err != nil { t.Fatalf("GenerateSecret failed: %v", err) } if seen[secret] { t.Errorf("duplicate secret generated: %s", secret) } seen[secret] = true } } func newTestManager(t *testing.T, initialYAML string) *Manager { t.Helper() tempDir := t.TempDir() secretsPath := filepath.Join(tempDir, "secrets.yaml") if err := storage.WriteFile(secretsPath, []byte(initialYAML), 0600); err != nil { t.Fatalf("setup failed: %v", err) } return NewManager(secretsPath) } func TestNewManager(t *testing.T) { m := NewManager("/tmp/test-secrets.yaml") if m == nil || m.yq == nil { t.Fatal("NewManager returned nil or Manager.yq is nil") } } func TestGetSecret(t *testing.T) { tests := []struct { name string secretsYAML string key string want string wantErr bool errContains string }{ { name: "get simple string value", secretsYAML: `cloudflare: apiToken: "my-secret-token" `, key: "cloudflare.apiToken", want: "my-secret-token", }, { name: "get nested value", secretsYAML: `cloudflare: nested: deep: "deep-value" `, key: "cloudflare.nested.deep", want: "deep-value", }, { name: "non-existent key returns error", secretsYAML: `cloudflare: apiToken: "value" `, key: "nonexistent", wantErr: true, errContains: "secret not found", }, { name: "empty string returns error", secretsYAML: `cloudflare: apiToken: "" `, key: "cloudflare.apiToken", wantErr: true, errContains: "secret not found", }, { name: "null value returns error", secretsYAML: `cloudflare: apiToken: null `, key: "cloudflare.apiToken", wantErr: true, errContains: "secret not found", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := newTestManager(t, tt.secretsYAML) got, err := m.GetSecret(tt.key) if tt.wantErr { if err == nil { t.Error("expected error, got nil") } else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { t.Errorf("error %q does not contain %q", err.Error(), tt.errContains) } return } if err != nil { t.Errorf("unexpected error: %v", err) return } if got != tt.want { t.Errorf("got %q, want %q", got, tt.want) } }) } } func TestGetSecret_NonExistentFile(t *testing.T) { m := NewManager(filepath.Join(t.TempDir(), "nonexistent.yaml")) _, err := m.GetSecret("some.key") if err == nil { t.Error("expected error, got nil") } else if !strings.Contains(err.Error(), "secrets file not found") { t.Errorf("error %q does not contain 'secrets file not found'", err.Error()) } } func TestGetSecret_NoSecretLeakage(t *testing.T) { secretValue := "super-secret-password-12345" m := newTestManager(t, `cloudflare: apiToken: "`+secretValue+`" `) _, err := m.GetSecret("nonexistent.key") if err == nil { t.Fatal("expected error, got nil") } if strings.Contains(err.Error(), secretValue) { t.Errorf("error message leaked secret value: %v", err) } } func TestSetSecret(t *testing.T) { tests := []struct { name string initialYAML string key string value string wantValue string }{ { name: "set simple value", initialYAML: `cloudflare: apiToken: "" `, key: "cloudflare.apiToken", value: "new-token", wantValue: "new-token", }, { name: "update existing value", initialYAML: `cloudflare: apiToken: "old-token" `, key: "cloudflare.apiToken", value: "new-token", wantValue: "new-token", }, { name: "create new nested path", initialYAML: `cloudflare: {} `, key: "cloudflare.newSecret", value: "newValue", wantValue: "newValue", }, { name: "value with special characters", initialYAML: `cloudflare: apiToken: "" `, key: "cloudflare.apiToken", value: `special"quotes'and\backslashes`, wantValue: `special"quotes'and\backslashes`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := newTestManager(t, tt.initialYAML) if err := m.SetSecret(tt.key, tt.value); err != nil { t.Errorf("SetSecret failed: %v", err) return } got, err := m.GetSecret(tt.key) if err != nil { t.Fatalf("verify failed: %v", err) } if got != tt.wantValue { t.Errorf("got %q, want %q", got, tt.wantValue) } // Verify permissions remain 0600 info, err := os.Stat(m.secretsPath) if err != nil { t.Fatalf("failed to stat secrets file: %v", err) } if info.Mode().Perm() != 0600 { t.Errorf("permissions changed: got %o, want 0600", info.Mode().Perm()) } }) } } func TestSetSecret_NonExistentFile(t *testing.T) { m := NewManager(filepath.Join(t.TempDir(), "nonexistent.yaml")) err := m.SetSecret("some.key", "value") if err == nil { t.Error("expected error, got nil") } else if !strings.Contains(err.Error(), "secrets file not found") { t.Errorf("error %q does not contain 'secrets file not found'", err.Error()) } } func TestSetSecret_ConcurrentAccess(t *testing.T) { m := newTestManager(t, `counter: "0" `) const numGoroutines = 10 var wg sync.WaitGroup errs := make(chan error, numGoroutines) for i := 0; i < numGoroutines; i++ { wg.Add(1) go func(val int) { defer wg.Done() value := string(rune('0' + val)) if err := m.SetSecret("counter", value); err != nil { errs <- err } }(i) } wg.Wait() close(errs) for err := range errs { t.Errorf("concurrent write error: %v", err) } info, err := os.Stat(m.secretsPath) if err != nil { t.Fatalf("failed to stat secrets file: %v", err) } if info.Mode().Perm() != 0600 { t.Errorf("permissions changed after concurrent writes: got %o, want 0600", info.Mode().Perm()) } value, err := m.GetSecret("counter") if err != nil { t.Errorf("failed to read value after concurrent writes: %v", err) } if value == "" || value == "null" { t.Error("counter value is empty after concurrent writes") } } func TestDeleteSecret(t *testing.T) { m := newTestManager(t, `cloudflare: apiToken: "token-to-delete" zoneId: "keep-this" `) // Verify secret exists _, err := m.GetSecret("cloudflare.apiToken") if err != nil { t.Fatalf("secret should exist before deletion: %v", err) } if err := m.DeleteSecret("cloudflare.apiToken"); err != nil { t.Errorf("DeleteSecret failed: %v", err) return } // Verify deleted _, err = m.GetSecret("cloudflare.apiToken") if err == nil { t.Error("secret should not exist after deletion") } // Verify other secret untouched kept, err := m.GetSecret("cloudflare.zoneId") if err != nil { t.Errorf("other secret should still exist: %v", err) } if kept != "keep-this" { t.Errorf("other secret changed: got %q", kept) } // Verify permissions info, err := os.Stat(m.secretsPath) if err != nil { t.Fatalf("failed to stat secrets file: %v", err) } if info.Mode().Perm() != 0600 { t.Errorf("permissions changed: got %o, want 0600", info.Mode().Perm()) } } func TestDeleteSecret_NonExistentFile(t *testing.T) { m := NewManager(filepath.Join(t.TempDir(), "nonexistent.yaml")) err := m.DeleteSecret("some.key") if err == nil { t.Error("expected error, got nil") } else if !strings.Contains(err.Error(), "secrets file not found") { t.Errorf("error %q does not contain 'secrets file not found'", err.Error()) } } func TestGetAll(t *testing.T) { m := newTestManager(t, `cloudflare: apiToken: "my-token" zoneId: "zone-123" topLevel: "value" `) all, err := m.GetAll() if err != nil { t.Fatalf("GetAll failed: %v", err) } if all["topLevel"] != "value" { t.Errorf("expected topLevel=value, got %v", all["topLevel"]) } cf, ok := all["cloudflare"].(map[string]any) if !ok { t.Fatalf("expected cloudflare to be a map, got %T", all["cloudflare"]) } if cf["apiToken"] != "my-token" { t.Errorf("expected apiToken=my-token, got %v", cf["apiToken"]) } } func TestGetAll_NonExistentFile(t *testing.T) { m := NewManager(filepath.Join(t.TempDir(), "nonexistent.yaml")) all, err := m.GetAll() if err != nil { t.Fatalf("expected no error for missing file, got: %v", err) } if len(all) != 0 { t.Errorf("expected empty map, got %v", all) } } func TestMergeUpdate(t *testing.T) { m := newTestManager(t, `cloudflare: apiToken: "original" existing: "keep" `) err := m.MergeUpdate(map[string]any{ "cloudflare": map[string]any{ "apiToken": "updated", }, "newKey": "newValue", }) if err != nil { t.Fatalf("MergeUpdate failed: %v", err) } all, err := m.GetAll() if err != nil { t.Fatalf("GetAll failed: %v", err) } if all["existing"] != "keep" { t.Errorf("existing key changed: got %v", all["existing"]) } if all["newKey"] != "newValue" { t.Errorf("expected newKey=newValue, got %v", all["newKey"]) } cf := all["cloudflare"].(map[string]any) if cf["apiToken"] != "updated" { t.Errorf("expected apiToken=updated, got %v", cf["apiToken"]) } // Verify permissions info, err := os.Stat(m.secretsPath) if err != nil { t.Fatalf("failed to stat: %v", err) } if info.Mode().Perm() != 0600 { t.Errorf("permissions changed: got %o, want 0600", info.Mode().Perm()) } } func TestMergeUpdate_CreatesFileContent(t *testing.T) { // Start with empty file m := newTestManager(t, ``) err := m.MergeUpdate(map[string]any{ "cloudflare": map[string]any{ "apiToken": "new-token", }, }) if err != nil { t.Fatalf("MergeUpdate failed: %v", err) } all, err := m.GetAll() if err != nil { t.Fatalf("GetAll failed: %v", err) } cf := all["cloudflare"].(map[string]any) if cf["apiToken"] != "new-token" { t.Errorf("expected apiToken=new-token, got %v", cf["apiToken"]) } }