package vault_test import ( "path/filepath" "slices" "testing" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/awnumar/memguard" "github.com/spf13/afero" ) // testMnemonic is the shared BIP39 test mnemonic for tests in this package. // //nolint:dupword // BIP39 test mnemonic intentionally repeats a word const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon " + "abandon abandon abandon abandon about" // Shared fixtures for tests in this package. const ( testStateDir = "/test/state" testVaultName = "test-vault" testSecretName = "test-secret" testPassphrase = "test-passphrase" ) //nolint:paralleltest // t.Setenv and order-dependent subtests forbid parallel func TestVaultOperations(t *testing.T) { // Test environment will be cleaned up automatically by t.Setenv t.Setenv(secret.EnvMnemonic, testMnemonic) t.Setenv(secret.EnvUnlockPassphrase, testPassphrase) // Use in-memory filesystem fs := afero.NewMemMapFs() t.Run("CreateVault", func(t *testing.T) { testCreateVault(t, fs) }) t.Run("ListVaults", func(t *testing.T) { testListVaults(t, fs) }) t.Run("SelectVault", func(t *testing.T) { testSelectVault(t, fs) }) t.Run("SecretOperations", func(t *testing.T) { testSecretOperations(t, fs) }) t.Run("NumSecrets", func(t *testing.T) { testNumSecrets(t, fs) }) t.Run("UnlockerOperations", func(t *testing.T) { testUnlockerOperations(t, fs) }) } func testCreateVault(t *testing.T, fs afero.Fs) { t.Helper() vlt, err := vault.CreateVault(fs, testStateDir, testVaultName) if err != nil { t.Fatalf("Failed to create vault: %v", err) } if vlt.GetName() != testVaultName { t.Errorf("Expected vault name '%s', got '%s'", testVaultName, vlt.GetName()) } // Check vault directory exists vaultDir, err := vlt.GetDirectory() if err != nil { t.Fatalf("Failed to get vault directory: %v", err) } exists, err := afero.DirExists(fs, vaultDir) if err != nil { t.Fatalf("Failed to check vault directory: %v", err) } if !exists { t.Errorf("Vault directory should exist") } } func testListVaults(t *testing.T, fs afero.Fs) { t.Helper() vaults, err := vault.ListVaults(fs, testStateDir) if err != nil { t.Fatalf("Failed to list vaults: %v", err) } if !slices.Contains(vaults, testVaultName) { t.Errorf("Expected to find '%s' in vault list", testVaultName) } } func testSelectVault(t *testing.T, fs afero.Fs) { t.Helper() err := vault.SelectVault(fs, testStateDir, testVaultName) if err != nil { t.Fatalf("Failed to select vault: %v", err) } // Test getting current vault currentVault, err := vault.GetCurrentVault(fs, testStateDir) if err != nil { t.Fatalf("Failed to get current vault: %v", err) } if currentVault.GetName() != testVaultName { t.Errorf("Expected current vault '%s', got '%s'", testVaultName, currentVault.GetName()) } } func testSecretOperations(t *testing.T, fs afero.Fs) { t.Helper() vlt, err := vault.GetCurrentVault(fs, testStateDir) if err != nil { t.Fatalf("Failed to get current vault: %v", err) } // First, derive the long-term key from the test mnemonic ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive long-term key: %v", err) } // Get the public key from the derived identity ltPublicKey := ltIdentity.Recipient().String() // Get the vault directory vaultDir, err := vlt.GetDirectory() if err != nil { t.Fatalf("Failed to get vault directory: %v", err) } // Write the correct public key to the pub.age file pubKeyPath := filepath.Join(vaultDir, "pub.age") err = afero.WriteFile(fs, pubKeyPath, []byte(ltPublicKey), secret.FilePerms) if err != nil { t.Fatalf("Failed to write long-term public key: %v", err) } // Unlock the vault with the derived identity vlt.Unlock(ltIdentity) // Now add a secret secretName := "test/secret" secretValue := []byte("test-secret-value") expectedValue := make([]byte, len(secretValue)) copy(expectedValue, secretValue) secretBuffer := memguard.NewBufferFromBytes(secretValue) defer secretBuffer.Destroy() err = vlt.AddSecret(secretName, secretBuffer, false) if err != nil { t.Fatalf("Failed to add secret: %v", err) } // List secrets secrets, err := vlt.ListSecrets() if err != nil { t.Fatalf("Failed to list secrets: %v", err) } if !slices.Contains(secrets, secretName) { t.Errorf("Expected to find secret '%s' in list", secretName) } // Get secret value retrievedValue, err := vlt.GetSecret(secretName) if err != nil { t.Fatalf("Failed to get secret: %v", err) } if string(retrievedValue) != string(expectedValue) { t.Errorf("Expected secret value '%s', got '%s'", string(expectedValue), string(retrievedValue)) } } func testNumSecrets(t *testing.T, fs afero.Fs) { t.Helper() vlt, err := vault.GetCurrentVault(fs, testStateDir) if err != nil { t.Fatalf("Failed to get current vault: %v", err) } numSecrets, err := vlt.NumSecrets() if err != nil { t.Fatalf("Failed to count secrets: %v", err) } // We added one secret in SecretOperations if numSecrets != 1 { t.Errorf("Expected 1 secret, got %d", numSecrets) } } func testUnlockerOperations(t *testing.T, fs afero.Fs) { t.Helper() vlt, err := vault.GetCurrentVault(fs, testStateDir) if err != nil { t.Fatalf("Failed to get current vault: %v", err) } // Test vault unlocking (should happen automatically via mnemonic) if vlt.Locked() { _, err := vlt.UnlockVault() if err != nil { t.Fatalf("Failed to unlock vault: %v", err) } } // Create a passphrase unlocker passphraseBuffer := memguard.NewBufferFromBytes([]byte(testPassphrase)) defer passphraseBuffer.Destroy() passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseBuffer) if err != nil { t.Fatalf("Failed to create passphrase unlocker: %v", err) } // List unlockers unlockers, err := vlt.ListUnlockers() if err != nil { t.Fatalf("Failed to list unlockers: %v", err) } if len(unlockers) == 0 { t.Errorf("Expected at least one unlocker") } // Check key type keyFound := false for _, key := range unlockers { if key.Type == "passphrase" { keyFound = true break } } if !keyFound { t.Errorf("Expected to find passphrase unlocker") } // Test selecting unlocker err = vlt.SelectUnlocker(passphraseUnlocker.GetID()) if err != nil { t.Fatalf("Failed to select unlocker: %v", err) } // Test getting current unlocker currentUnlocker, err := vlt.GetCurrentUnlocker() if err != nil { t.Fatalf("Failed to get current unlocker: %v", err) } if currentUnlocker.GetID() != passphraseUnlocker.GetID() { t.Errorf("Expected current unlocker ID '%s', got '%s'", passphraseUnlocker.GetID(), currentUnlocker.GetID()) } } func TestListUnlockers_SkipsMissingMetadata(t *testing.T) { // Set test environment variables t.Setenv(secret.EnvMnemonic, testMnemonic) t.Setenv(secret.EnvUnlockPassphrase, testPassphrase) // Use in-memory filesystem fs := afero.NewMemMapFs() // Create vault vlt, err := vault.CreateVault(fs, testStateDir, testVaultName) 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(testPassphrase)) 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") } } }