diff --git a/internal/secret/debug.go b/internal/secret/debug.go index e85b8dc..c38d4ad 100644 --- a/internal/secret/debug.go +++ b/internal/secret/debug.go @@ -58,6 +58,16 @@ func IsDebugEnabled() bool { return debugEnabled } +// Warn logs a warning message to stderr unconditionally (visible without --verbose or debug flags) +func Warn(msg string, args ...any) { + output := fmt.Sprintf("WARNING: %s", msg) + for i := 0; i+1 < len(args); i += 2 { + output += fmt.Sprintf(" %s=%v", args[i], args[i+1]) + } + output += "\n" + fmt.Fprint(os.Stderr, output) +} + // Debug logs a debug message with optional attributes func Debug(msg string, args ...any) { if !debugEnabled { diff --git a/internal/vault/unlockers.go b/internal/vault/unlockers.go index b7a2087..a20ce41 100644 --- a/internal/vault/unlockers.go +++ b/internal/vault/unlockers.go @@ -213,7 +213,9 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) { return nil, fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err) } if !exists { - return nil, fmt.Errorf("unlocker directory %s is missing metadata file", file.Name()) + secret.Warn("Skipping unlocker directory with missing metadata file", "directory", file.Name()) + + continue } metadataBytes, err := afero.ReadFile(v.fs, metadataPath) diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go index a69bbdf..15ac662 100644 --- a/internal/vault/vault_test.go +++ b/internal/vault/vault_test.go @@ -243,3 +243,57 @@ func TestVaultOperations(t *testing.T) { } }) } + +func TestListUnlockers_SkipsMissingMetadata(t *testing.T) { + // Set test environment variables + testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + t.Setenv(secret.EnvMnemonic, testMnemonic) + t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase") + + // Use in-memory filesystem + fs := afero.NewMemMapFs() + stateDir := "/test/state" + + // Create vault + vlt, err := CreateVault(fs, stateDir, "test-vault") + if err != nil { + t.Fatalf("Failed to create vault: %v", err) + } + + // Create a passphrase unlocker so we have at least one valid unlocker + passphraseBuffer := memguard.NewBufferFromBytes([]byte("test-passphrase")) + defer passphraseBuffer.Destroy() + _, err = vlt.CreatePassphraseUnlocker(passphraseBuffer) + if err != nil { + t.Fatalf("Failed to create passphrase unlocker: %v", err) + } + + // Create a bogus unlocker directory with no metadata file + vaultDir, err := vlt.GetDirectory() + if err != nil { + t.Fatalf("Failed to get vault directory: %v", err) + } + bogusDir := filepath.Join(vaultDir, "unlockers.d", "bogus-no-metadata") + err = fs.MkdirAll(bogusDir, 0o700) + if err != nil { + t.Fatalf("Failed to create bogus directory: %v", err) + } + + // ListUnlockers should succeed, skipping the bogus directory + unlockers, err := vlt.ListUnlockers() + if err != nil { + t.Fatalf("ListUnlockers returned error when it should have skipped bad directory: %v", err) + } + + // Should still have the valid passphrase unlocker + if len(unlockers) == 0 { + t.Errorf("Expected at least one unlocker, got none") + } + + // Verify we only got the valid unlocker(s), not the bogus one + for _, u := range unlockers { + if u.Type == "" { + t.Errorf("Got unlocker with empty type, likely from bogus directory") + } + } +}