Check errors by identity, not by message text, in tests (closes #49)
check / check (push) Failing after 1s
check / check (push) Failing after 1s
Tests that asserted a failure by a fragment of its message now use errors.Is: a refactor returning the wrong error, or wrapping with %v instead of %w, now fails them. New tests return each exported error of internal/vault and pkg/bip85 that no test returned, and check wrapped causes (os.ErrNotExist, ErrMnemonicMismatch through GetSecret, ErrInvalidPathComponent through DeriveBIP85Entropy). The 999-versions test moves into package secret to name its unexported error. Checks of errors no test can name keep their text; they are listed on the issue. Model: opus-5-5
This commit was merged in pull request #112.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package vault_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"git.eeqj.de/sneak/secret/internal/vault"
|
||||
"github.com/awnumar/memguard"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
// otherMnemonic is a valid BIP39 mnemonic other than testMnemonic.
|
||||
otherMnemonic = "legal winner thank year wave sausage worth useful " +
|
||||
"legal winner thank yellow"
|
||||
|
||||
// missingName names no vault, secret or unlocker.
|
||||
missingName = "missing"
|
||||
)
|
||||
|
||||
// newErrorTestVault creates the vault testVaultName, with the secret
|
||||
// testSecretName in it, on a new in-memory filesystem.
|
||||
func newErrorTestVault(t *testing.T) *vault.Vault {
|
||||
t.Helper()
|
||||
|
||||
vlt, err := vault.CreateVault(afero.NewMemMapFs(), testStateDir,
|
||||
testVaultName, testMnemonicBuffer(t), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
value := memguard.NewBufferFromBytes([]byte("value"))
|
||||
t.Cleanup(value.Destroy)
|
||||
|
||||
require.NoError(t, vlt.AddSecret(testSecretName, value, false))
|
||||
|
||||
return vlt
|
||||
}
|
||||
|
||||
// TestVaultErrors checks that each failure returns its exported error,
|
||||
// wrapped or not, so that errors.Is tells it apart from the others.
|
||||
func TestVaultErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
vaultDir := filepath.Join(testStateDir, "vaults.d", testVaultName)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
run func(vlt *vault.Vault) error
|
||||
want error
|
||||
}{
|
||||
{"create an existing vault", func(vlt *vault.Vault) error {
|
||||
_, err := vault.CreateVault(vlt.GetFilesystem(), testStateDir,
|
||||
testVaultName, nil, nil)
|
||||
|
||||
return err
|
||||
}, vault.ErrVaultExists},
|
||||
{"select a missing vault", func(vlt *vault.Vault) error {
|
||||
return vault.SelectVault(vlt.GetFilesystem(), testStateDir, missingName)
|
||||
}, vault.ErrVaultNotFound},
|
||||
{"add a nil value", func(vlt *vault.Vault) error {
|
||||
return vlt.AddSecret(missingName, nil, false)
|
||||
}, vault.ErrNilValueBuffer},
|
||||
{"get a missing secret", func(vlt *vault.Vault) error {
|
||||
_, err := vlt.GetSecret(missingName)
|
||||
|
||||
return err
|
||||
}, vault.ErrSecretNotFound},
|
||||
{"copy onto an existing secret", func(vlt *vault.Vault) error {
|
||||
return vlt.CopySecretAllVersions(vlt, testSecretName, testSecretName, false)
|
||||
}, vault.ErrSecretExists},
|
||||
{"copy a secret without versions", func(vlt *vault.Vault) error {
|
||||
const versionless = "versionless"
|
||||
|
||||
err := vlt.GetFilesystem().MkdirAll(
|
||||
filepath.Join(vaultDir, "secrets.d", versionless), secret.DirPerms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return vlt.CopySecretAllVersions(vlt, versionless, "copy", false)
|
||||
}, vault.ErrNoVersions},
|
||||
{"remove a missing unlocker", func(vlt *vault.Vault) error {
|
||||
return vlt.RemoveUnlocker(missingName)
|
||||
}, vault.ErrUnlockerNotFound},
|
||||
{"select a missing unlocker", func(vlt *vault.Vault) error {
|
||||
return vlt.SelectUnlocker(missingName)
|
||||
}, vault.ErrUnlockerNotFound},
|
||||
{"unlocker of an unknown type", func(vlt *vault.Vault) error {
|
||||
fs := vlt.GetFilesystem()
|
||||
|
||||
err := afero.WriteFile(fs,
|
||||
filepath.Join(vaultDir, "unlockers.d", "odd", "unlocker-metadata.json"),
|
||||
[]byte(`{"type":"odd"}`), secret.FilePerms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = afero.WriteFile(fs, filepath.Join(vaultDir, "current-unlocker"),
|
||||
[]byte("odd"), secret.FilePerms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = vlt.GetCurrentUnlocker()
|
||||
|
||||
return err
|
||||
}, vault.ErrUnsupportedUnlockerType},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.ErrorIs(t, tt.run(newErrorTestVault(t)), tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetSecretWithWrongMnemonic checks that getting a secret that exists,
|
||||
// from a vault the given mnemonic does not open, fails with
|
||||
// ErrMnemonicMismatch through GetSecret's wrapping, and not with
|
||||
// ErrSecretNotFound.
|
||||
func TestGetSecretWithWrongMnemonic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
created := newErrorTestVault(t)
|
||||
|
||||
mnemonic := memguard.NewBufferFromBytes([]byte(otherMnemonic))
|
||||
t.Cleanup(mnemonic.Destroy)
|
||||
|
||||
vlt := vault.NewVault(created.GetFilesystem(), testStateDir, testVaultName)
|
||||
vlt.SetMnemonic(mnemonic)
|
||||
|
||||
_, err := vlt.GetSecret(testSecretName)
|
||||
require.ErrorIs(t, err, vault.ErrMnemonicMismatch)
|
||||
require.NotErrorIs(t, err, vault.ErrSecretNotFound)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package vault_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -341,9 +342,9 @@ func testVaultNameValidation(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
|
||||
for _, name := range invalidNames {
|
||||
_, err := vault.CreateVault(fs, stateDir, name, testMnemonicBuffer(t), nil)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error creating vault with invalid name %q, "+
|
||||
"but got none", name)
|
||||
if !errors.Is(err, vault.ErrInvalidVaultName) {
|
||||
t.Errorf("Expected ErrInvalidVaultName creating vault with "+
|
||||
"invalid name %q, got %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,10 +320,10 @@ func testVersionSerialLimits(
|
||||
err = fs.MkdirAll(filepath.Join(secretDir, versionName), 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should fail to create 1000th version
|
||||
// Should fail to create 1000th version. The error is unexported in
|
||||
// package secret, whose own test checks that it is the one returned.
|
||||
_, err = secret.GenerateVersionName(fs, filepath.Dir(secretDir))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "exceeded maximum versions per day")
|
||||
}
|
||||
|
||||
func testVersionErrorCases(t *testing.T, vault *Vault, secretName string) {
|
||||
@@ -331,20 +331,18 @@ func testVersionErrorCases(t *testing.T, vault *Vault, secretName string) {
|
||||
|
||||
// Try to get non-existent version
|
||||
_, err := vault.GetSecretVersion(secretName, "99991231.999")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
require.ErrorIs(t, err, ErrVersionNotFound)
|
||||
|
||||
// Try to get version of non-existent secret
|
||||
_, err = vault.GetSecretVersion("nonexistent/secret", "")
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrSecretNotFound)
|
||||
|
||||
// Try to add secret without force when it exists
|
||||
failBuffer := memguard.NewBufferFromBytes([]byte("should-fail"))
|
||||
defer failBuffer.Destroy()
|
||||
|
||||
err = vault.AddSecret(secretName, failBuffer, false)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already exists")
|
||||
require.ErrorIs(t, err, ErrSecretExists)
|
||||
}
|
||||
|
||||
// TestVersionConcurrency tests concurrent version operations
|
||||
|
||||
@@ -41,10 +41,8 @@ func TestGetSecretVersionRejectsPathTraversal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := vlt.GetSecretVersion(name, "")
|
||||
require.Error(t, err,
|
||||
require.ErrorIs(t, err, vault.ErrInvalidSecretName,
|
||||
"GetSecretVersion should reject malicious name: %s", name)
|
||||
require.Contains(t, err.Error(), "invalid secret name",
|
||||
"error should indicate invalid name for: %s", name)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -61,8 +59,7 @@ func TestGetSecretRejectsPathTraversal(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = vlt.GetSecret("../../../etc/passwd")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "invalid secret name")
|
||||
require.ErrorIs(t, err, vault.ErrInvalidSecretName)
|
||||
}
|
||||
|
||||
// TestGetSecretObjectRejectsPathTraversal verifies GetSecretObject
|
||||
@@ -87,8 +84,8 @@ func TestGetSecretObjectRejectsPathTraversal(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := vlt.GetSecretObject(name)
|
||||
require.Error(t, err, "GetSecretObject should reject: %s", name)
|
||||
require.Contains(t, err.Error(), "invalid secret name")
|
||||
require.ErrorIs(t, err, vault.ErrInvalidSecretName,
|
||||
"GetSecretObject should reject: %s", name)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +143,7 @@ func TestVaultAddSecretMultipleVersions(t *testing.T) {
|
||||
defer failBuffer.Destroy()
|
||||
|
||||
err := vault.AddSecret(testSecretPath, failBuffer, false)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already exists")
|
||||
require.ErrorIs(t, err, ErrSecretExists)
|
||||
|
||||
// Add with force - should create new version
|
||||
addTestSecretToVault(t, vault, testSecretPath, []byte("version-2"), true)
|
||||
@@ -309,8 +308,7 @@ func TestVaultGetNonExistentVersion(t *testing.T) {
|
||||
|
||||
// Try to get non-existent version
|
||||
_, err := vault.GetSecretVersion(testSecretPath, "20991231.999")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
require.ErrorIs(t, err, ErrVersionNotFound)
|
||||
}
|
||||
|
||||
func TestUpdateVersionMetadata(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package vault_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -36,8 +37,13 @@ func TestAddSecretFailsWithMissingPublicKey(t *testing.T) {
|
||||
defer value.Destroy()
|
||||
|
||||
err := vlt.AddSecret(testSecretName, value, false)
|
||||
require.Error(t, err, "AddSecret should fail when public key is missing")
|
||||
assert.Contains(t, err.Error(), "failed to read long-term public key")
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user