package vault_test import ( "os" "path/filepath" "testing" "filippo.io/age" "github.com/awnumar/memguard" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/secret/internal/secret" "sneak.berlin/go/secret/internal/vault" ) func TestAddSecretFailsWithMissingPublicKey(t *testing.T) { t.Parallel() // Create in-memory filesystem fs := afero.NewMemMapFs() // Create a vault directory without a public key (simulating the error // condition) vaultDir := filepath.Join(testStateDir, "vaults.d", "broken") require.NoError(t, fs.MkdirAll(vaultDir, secret.DirPerms)) // Create currentvault symlink currentVaultPath := filepath.Join(testStateDir, "currentvault") require.NoError(t, afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), secret.FilePerms)) // Create vault instance vlt := vault.NewVault(fs, testStateDir, "broken") // Try to add a secret - this should fail value := memguard.NewBufferFromBytes([]byte("test-value")) defer value.Destroy() err := vlt.AddSecret(testSecretName, value, false) var cause *os.PathError require.ErrorAs(t, err, &cause) require.ErrorIs(t, err, os.ErrNotExist, "AddSecret should fail when public key is missing") assert.Equal(t, filepath.Join(vaultDir, "pub.age"), cause.Path) // Verify that the secret directory was NOT created secretDir := filepath.Join(vaultDir, "secrets.d", testSecretName) exists, _ := afero.DirExists(fs, secretDir) assert.False(t, exists, "Secret directory should not exist after failed AddSecret") // Verify the secrets.d directory is empty or doesn't exist secretsDir := filepath.Join(vaultDir, "secrets.d") if exists, _ := afero.DirExists(fs, secretsDir); exists { entries, err := afero.ReadDir(fs, secretsDir) require.NoError(t, err) assert.Empty(t, entries, "secrets.d directory should be empty after failed AddSecret") } } func TestAddSecretCleansUpOnFailure(t *testing.T) { t.Parallel() // Create in-memory filesystem fs := afero.NewMemMapFs() // Create a vault directory with public key vaultDir := filepath.Join(testStateDir, "vaults.d", "test") require.NoError(t, fs.MkdirAll(vaultDir, secret.DirPerms)) // Create a mock public key that will cause encryption to fail // by using an invalid age public key format pubKeyPath := filepath.Join(vaultDir, "pub.age") require.NoError(t, afero.WriteFile(fs, pubKeyPath, []byte("invalid-public-key"), secret.FilePerms)) // Create currentvault symlink currentVaultPath := filepath.Join(testStateDir, "currentvault") require.NoError(t, afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), secret.FilePerms)) // Create vault instance vlt := vault.NewVault(fs, testStateDir, "test") // Try to add a secret - this should fail during encryption value := memguard.NewBufferFromBytes([]byte("test-value")) defer value.Destroy() err := vlt.AddSecret(testSecretName, value, false) require.Error(t, err, "AddSecret should fail with invalid public key") // Verify that the secret directory was NOT created secretDir := filepath.Join(vaultDir, "secrets.d", testSecretName) exists, _ := afero.DirExists(fs, secretDir) assert.False(t, exists, "Secret directory should not exist after failed AddSecret") // Nor is the temporary directory the secret was assembled in left behind entries, err := afero.ReadDir(fs, vaultDir) require.NoError(t, err) require.Len(t, entries, 1) assert.Equal(t, "pub.age", entries[0].Name()) } // TestAddSecretForceWithWrongPassphraseChangesNothing checks that adding a // version to an existing secret, with a passphrase that does not open the // vault, fails and leaves the secret's directory as it was. func TestAddSecretForceWithWrongPassphraseChangesNothing(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase)) t.Cleanup(passphrase.Destroy) created, err := vault.CreateVault(fs, testStateDir, testVaultName, testMnemonicBuffer(t), passphrase) require.NoError(t, err) first := memguard.NewBufferFromBytes([]byte("first")) t.Cleanup(first.Destroy) require.NoError(t, created.AddSecret(testSecretName, first, false)) wrong := memguard.NewBufferFromBytes([]byte("wrong passphrase")) t.Cleanup(wrong.Destroy) vlt := vault.NewVault(fs, testStateDir, testVaultName) vlt.SetUnlockPassphrase(wrong) secretDir := filepath.Join(testStateDir, "vaults.d", testVaultName, "secrets.d", testSecretName) before := dirContents(t, fs, secretDir) second := memguard.NewBufferFromBytes([]byte("second")) t.Cleanup(second.Destroy) err = vlt.AddSecret(testSecretName, second, true) var noMatch *age.NoIdentityMatchError require.ErrorAs(t, err, &noMatch) assert.Equal(t, before, dirContents(t, fs, secretDir)) } // dirContents maps every file under dir to its contents, and every // directory, written with a trailing "/", to "". func dirContents(t *testing.T, fs afero.Fs, dir string) map[string]string { t.Helper() contents := map[string]string{} err := afero.Walk(fs, dir, func( path string, info os.FileInfo, err error, ) error { if err != nil { return err } if info.IsDir() { contents[path+"/"] = "" return nil } content, err := afero.ReadFile(fs, path) if err != nil { return err } contents[path] = string(content) return nil }) require.NoError(t, err) return contents }