Files
secret/internal/vault/vault_error_test.go
T
clawbot fac2753819
check / check (push) Waiting to run
Ask for the mnemonic and passphrase before taking the state directory lock (closes #152)
init, vault create, unlocker add, and add, import and generate secret
with --force asked for the mnemonic or passphrase, or unlocked the
vault, while holding the state directory lock, so every other command
that changes anything waited at the prompt. They now ask and unlock
first and lock after. Under the lock, unlocker add fails if the vault
was removed, or created again with another key, and refuses a GPG key
added meanwhile; a forced add of a secret created meanwhile lets go of
the lock, gets the key and locks again. secret.CreatePGPUnlocker takes
the unlocked vault; SetMnemonic and SetUnlockPassphrase are gone.

Not done: secret move between vaults still unlocks both under the lock
(#189).

Model: opus-5-5
2026-10-15 09:02:13 +02:00

179 lines
5.2 KiB
Go

package vault_test
import (
"os"
"path/filepath"
"testing"
"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.UnlockPassphrase = 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)
require.ErrorIs(t, err, secret.ErrWrongPassphrase)
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
}