Update golangci-lint to v2.12.2 with canonical config (#29)
All checks were successful
check / check (push) Successful in 43s
All checks were successful
check / check (push) Successful in 43s
Bumps golangci-lint from v2.1.6 (digest-only pin in the `Dockerfile` lint stage) to v2.12.2, pinned by tag and digest (Debian-based image). Replaces `.golangci.yml` with the canonical strict config: all linters enabled except the standard disable list (`exhaustruct`, `depguard`, `godot`, `wsl`, `wrapcheck`, `varnamelen`), `lll` at 88, `funlen` 80/50, `cyclop` 15, `dupl` 100, and test files are now linted (the old config had `tests: false`, an enable-only list of ~20 linters, `lll` 120, and a blanket exclusion of `internal/macse`). The stricter config surfaced ~1550 findings, all fixed: - `wsl_v5` (439) / `nlreturn` (24): blank-line insertions - `lll` (309): line wrapping at 88 columns; long literals split with `+` concatenation, values unchanged - `noinlineerr` (130): `if err := ...` split into assignment plus check - `paralleltest` (116): `t.Parallel()` added to tests without shared state; reasoned `//nolint` where `t.Setenv` or shared fixtures forbid it - `err113` (97): package-level sentinel errors (new `internal/vault/errors.go`), `%w` wrapping, `errors.Is` - `perfsprint` (74) / `modernize` (39) / `intrange`: `strconv`, `errors.New`, `slices.Contains`, `any`, `SplitSeq` - `goconst` (40) / `dupword` (41) / `testifylint` (42) / `thelper` (33): constants, assertion fixes, `t.Helper()` - `noctx` (22): `exec.CommandContext` for gpg/CLI invocations - `testpackage` (18): black-box tests moved to `_test` packages where they use only exported identifiers; white-box files carry a reasoned `//nolint` - `funlen`/`cyclop`/`gocognit`/`nestif`/`dupl`: behavior-preserving helper extraction - assorted singletons: `gosec`, `gosmopolitan`, `funcorder`, `nonamedreturns`, `makezero`, `prealloc`, `godox`, `nolintlint`, `ireturn`, `nilnil`, `gochecknoinits` ## User-visible strings **None changed.** Every error message this branch composes is byte-identical to the one `main` composes. The `err113` sentinels are shaped so `fmt.Errorf` reassembles the original text around them: the sentinel carries the fixed words and the caller supplies the interpolated value in the position it has always occupied. Where the value sits mid-sentence the sentinel holds only a fragment (e.g. `vault.ErrVaultNotFound` is `"does not exist"`, composed by its caller as `vault <name> does not exist`); each such sentinel documents the message it participates in. Verified mechanically, not by inspection: every `fmt.Errorf` and `errors.New` call site in both trees is parsed, the `Error()` text of any sentinel passed to `%w` is substituted in, and the resulting sets of composed message templates are compared. All 350 templates `main` produces are still produced, character for character. The set of lost or altered messages is empty. ## `unlocker list` `findUnlockerIDByMetadata` returns `(string, error)` rather than signalling failure with an empty ID, so an unreadable `unlockers.d` is no longer indistinguishable from "no matching entry". `UnlockersList` skips such an entry with a warning naming the directory — its behavior before the scan was extracted into a helper — instead of emitting a row under a synthesized fallback ID that no `unlocker remove` or `unlocker select` can match and that suppresses the current-unlocker marker. The duplicate-check and shell-completion callers skip on the same condition, matching their pre-extraction behavior. Covered by `internal/cli/unlockers_list_test.go`. `TODO.md` records the change plus follow-ups (version-completion TODOs formerly in code comments, darwin-gated files exceeding 88 columns that Linux CI does not lint). `make check` is green and the pinned v2.12.2 image reports `0 issues.` Note the test suite needs the memlock ulimit from `script/cibuild` for the 10MB memguard test; that requirement is pre-existing. Not changed: `script/bootstrap` installs golangci-lint via the system package manager (no version pin to bump), and `script/lint` invokes whatever `golangci-lint` is on PATH. golangci-lint v2.12 deprecates `gomodguard` in favor of `gomodguard_v2` (warning only); the canonical config owns that decision. Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #29 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
This commit was merged in pull request #29.
This commit is contained in:
@@ -3,8 +3,10 @@ package vault_test
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"git.eeqj.de/sneak/secret/internal/vault"
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
@@ -12,6 +14,33 @@ import (
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// deriveVaultIdentity derives the long-term identity for the given vault
|
||||
// from testMnemonic using the derivation index stored in its metadata.
|
||||
func deriveVaultIdentity(
|
||||
t *testing.T, fs afero.Fs, vlt *vault.Vault,
|
||||
) *age.X25519Identity {
|
||||
t.Helper()
|
||||
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
|
||||
vaultMetadata, err := vault.LoadVaultMetadata(fs, vaultDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load vault metadata: %v", err)
|
||||
}
|
||||
|
||||
ltIdentity, err := agehd.DeriveIdentity(testMnemonic,
|
||||
vaultMetadata.DerivationIndex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
return ltIdentity
|
||||
}
|
||||
|
||||
//nolint:paralleltest // t.Setenv forbids parallel subtests
|
||||
func TestVaultWithRealFilesystem(t *testing.T) {
|
||||
// Create a temporary directory for our tests
|
||||
tempDir := t.TempDir()
|
||||
@@ -19,398 +48,410 @@ func TestVaultWithRealFilesystem(t *testing.T) {
|
||||
// Use the real filesystem
|
||||
fs := afero.NewOsFs()
|
||||
|
||||
// Test mnemonic
|
||||
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
// Set test environment variables
|
||||
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
||||
t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase")
|
||||
t.Setenv(secret.EnvUnlockPassphrase, testPassphrase)
|
||||
|
||||
// Test currentvault file handling (plain file with relative path)
|
||||
t.Run("CurrentVaultFileHandling", func(t *testing.T) {
|
||||
stateDir := filepath.Join(tempDir, "currentvault-test")
|
||||
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a test vault
|
||||
vlt, err := vault.CreateVault(fs, stateDir, "test-vault")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
}
|
||||
|
||||
// Get the vault directory
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
|
||||
// Verify the currentvault file exists and contains just the vault name
|
||||
currentVaultPath := filepath.Join(stateDir, "currentvault")
|
||||
currentVaultContents, err := os.ReadFile(currentVaultPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read currentvault file: %v", err)
|
||||
}
|
||||
|
||||
expectedVaultName := "test-vault"
|
||||
if string(currentVaultContents) != expectedVaultName {
|
||||
t.Errorf("Expected currentvault to contain %q, got %q", expectedVaultName, string(currentVaultContents))
|
||||
}
|
||||
|
||||
// Test that ResolveVaultSymlink correctly resolves the path
|
||||
resolvedPath, err := vault.ResolveVaultSymlink(fs, currentVaultPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to resolve currentvault path: %v", err)
|
||||
}
|
||||
|
||||
if resolvedPath != vaultDir {
|
||||
t.Errorf("Expected resolved path to be %s, got %s", vaultDir, resolvedPath)
|
||||
}
|
||||
testCurrentVaultFileHandling(t, fs, tempDir)
|
||||
})
|
||||
|
||||
// Test secret operations with deeply nested paths
|
||||
t.Run("DeepPathSecrets", func(t *testing.T) {
|
||||
stateDir := filepath.Join(tempDir, "deep-path-test")
|
||||
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a test vault - CreateVault now handles public key when mnemonic is in env
|
||||
vlt, err := vault.CreateVault(fs, stateDir, "test-vault")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
}
|
||||
|
||||
// Load vault metadata to get its derivation index
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
vaultMetadata, err := vault.LoadVaultMetadata(fs, vaultDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load vault metadata: %v", err)
|
||||
}
|
||||
|
||||
// Derive long-term key from mnemonic using the vault's derivation index
|
||||
ltIdentity, err := agehd.DeriveIdentity(testMnemonic, vaultMetadata.DerivationIndex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Unlock the vault
|
||||
vlt.Unlock(ltIdentity)
|
||||
|
||||
// Create a secret with a deeply nested path
|
||||
deepPath := "api/credentials/production/database/primary"
|
||||
secretValue := []byte("supersecretdbpassword")
|
||||
expectedValue := make([]byte, len(secretValue))
|
||||
copy(expectedValue, secretValue)
|
||||
|
||||
secretBuffer := memguard.NewBufferFromBytes(secretValue)
|
||||
defer secretBuffer.Destroy()
|
||||
|
||||
err = vlt.AddSecret(deepPath, secretBuffer, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add secret with deep path: %v", err)
|
||||
}
|
||||
|
||||
// List secrets and verify our deep path secret is there
|
||||
secrets, err := vlt.ListSecrets()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list secrets: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, s := range secrets {
|
||||
if s == deepPath {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("Deep path secret not found in listed secrets")
|
||||
}
|
||||
|
||||
// Retrieve the secret and verify its value
|
||||
retrievedValue, err := vlt.GetSecret(deepPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve deep path secret: %v", err)
|
||||
}
|
||||
|
||||
if string(retrievedValue) != string(expectedValue) {
|
||||
t.Errorf("Retrieved value doesn't match. Expected %q, got %q",
|
||||
string(expectedValue), string(retrievedValue))
|
||||
}
|
||||
testDeepPathSecrets(t, fs, tempDir)
|
||||
})
|
||||
|
||||
// Test key caching in GetOrDeriveLongTermKey
|
||||
t.Run("KeyCaching", func(t *testing.T) {
|
||||
stateDir := filepath.Join(tempDir, "key-cache-test")
|
||||
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a test vault - CreateVault now handles public key when mnemonic is in env
|
||||
vlt, err := vault.CreateVault(fs, stateDir, "test-vault")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
}
|
||||
|
||||
// Load vault metadata to get its derivation index
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
vaultMetadata, err := vault.LoadVaultMetadata(fs, vaultDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load vault metadata: %v", err)
|
||||
}
|
||||
|
||||
// Derive long-term key from mnemonic for verification using the vault's derivation index
|
||||
ltIdentity, err := agehd.DeriveIdentity(testMnemonic, vaultMetadata.DerivationIndex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Verify the vault is locked initially
|
||||
if !vlt.Locked() {
|
||||
t.Errorf("Vault should be locked initially")
|
||||
}
|
||||
|
||||
// First call to GetOrDeriveLongTermKey should derive and cache the key
|
||||
firstKey, err := vlt.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Verify the vault is now unlocked
|
||||
if vlt.Locked() {
|
||||
t.Errorf("Vault should be unlocked after GetOrDeriveLongTermKey")
|
||||
}
|
||||
|
||||
// Second call should return the cached key without re-deriving
|
||||
secondKey, err := vlt.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get cached long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Verify both keys are the same instance
|
||||
if firstKey != secondKey {
|
||||
t.Errorf("Second key call should return same instance as first call")
|
||||
}
|
||||
|
||||
// Verify the public key matches what we expect
|
||||
expectedPubKey := ltIdentity.Recipient().String()
|
||||
actualPubKey := firstKey.Recipient().String()
|
||||
if actualPubKey != expectedPubKey {
|
||||
t.Errorf("Public key mismatch. Expected %s, got %s", expectedPubKey, actualPubKey)
|
||||
}
|
||||
|
||||
// Now clear the key and verify it's locked again
|
||||
vlt.ClearLongTermKey()
|
||||
if !vlt.Locked() {
|
||||
t.Errorf("Vault should be locked after clearing key")
|
||||
}
|
||||
|
||||
// Get the key again and verify it works
|
||||
thirdKey, err := vlt.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to re-derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Verify the public key still matches
|
||||
actualPubKey = thirdKey.Recipient().String()
|
||||
if actualPubKey != expectedPubKey {
|
||||
t.Errorf("Re-derived public key mismatch. Expected %s, got %s", expectedPubKey, actualPubKey)
|
||||
}
|
||||
testKeyCaching(t, fs, tempDir)
|
||||
})
|
||||
|
||||
// Test vault name validation
|
||||
t.Run("VaultNameValidation", func(t *testing.T) {
|
||||
stateDir := filepath.Join(tempDir, "name-validation-test")
|
||||
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Test valid vault names
|
||||
validNames := []string{
|
||||
"default",
|
||||
"test-vault",
|
||||
"production.vault",
|
||||
"vault_123",
|
||||
"a-very-long-vault-name-with-dashes",
|
||||
}
|
||||
|
||||
for _, name := range validNames {
|
||||
_, err := vault.CreateVault(fs, stateDir, name)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create vault with valid name %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test invalid vault names
|
||||
invalidNames := []string{
|
||||
"", // Empty
|
||||
"UPPERCASE", // Uppercase not allowed
|
||||
"invalid/name", // Slashes not allowed in vault names
|
||||
"invalid name", // Spaces not allowed
|
||||
"invalid@name", // Special chars not allowed
|
||||
}
|
||||
|
||||
for _, name := range invalidNames {
|
||||
_, err := vault.CreateVault(fs, stateDir, name)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error creating vault with invalid name %q, but got none", name)
|
||||
}
|
||||
}
|
||||
testVaultNameValidation(t, fs, tempDir)
|
||||
})
|
||||
|
||||
// Test multiple vaults and switching between them
|
||||
t.Run("MultipleVaults", func(t *testing.T) {
|
||||
stateDir := filepath.Join(tempDir, "multi-vault-test")
|
||||
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create three vaults
|
||||
vaultNames := []string{"vault1", "vault2", "vault3"}
|
||||
for _, name := range vaultNames {
|
||||
_, err := vault.CreateVault(fs, stateDir, name)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// List vaults and verify all three are there
|
||||
vaults, err := vault.ListVaults(fs, stateDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list vaults: %v", err)
|
||||
}
|
||||
|
||||
if len(vaults) != 3 {
|
||||
t.Errorf("Expected 3 vaults, got %d", len(vaults))
|
||||
}
|
||||
|
||||
// Test switching between vaults
|
||||
for _, name := range vaultNames {
|
||||
// Select the vault
|
||||
if err := vault.SelectVault(fs, stateDir, name); err != nil {
|
||||
t.Fatalf("Failed to select vault %s: %v", name, err)
|
||||
}
|
||||
|
||||
// Get current vault and verify it's the one we selected
|
||||
currentVault, err := vault.GetCurrentVault(fs, stateDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current vault after selecting %s: %v", name, err)
|
||||
}
|
||||
|
||||
if currentVault.GetName() != name {
|
||||
t.Errorf("Expected current vault to be %s, got %s", name, currentVault.GetName())
|
||||
}
|
||||
}
|
||||
testMultipleVaults(t, fs, tempDir)
|
||||
})
|
||||
|
||||
// Test adding a secret in one vault and verifying it's not visible in another
|
||||
// Test adding a secret in one vault and verifying it's not visible in
|
||||
// another
|
||||
t.Run("VaultIsolation", func(t *testing.T) {
|
||||
stateDir := filepath.Join(tempDir, "isolation-test")
|
||||
if err := os.MkdirAll(stateDir, 0o700); err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create two vaults - CreateVault now handles public key when mnemonic is in env
|
||||
vault1, err := vault.CreateVault(fs, stateDir, "vault1")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault1: %v", err)
|
||||
}
|
||||
|
||||
vault2, err := vault.CreateVault(fs, stateDir, "vault2")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault2: %v", err)
|
||||
}
|
||||
|
||||
// Derive long-term key from mnemonic
|
||||
// Note: Both vaults will have different derivation indexes due to GetNextDerivationIndex
|
||||
|
||||
// Load vault1 metadata to get its derivation index
|
||||
vault1Dir, err := vault1.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault1 directory: %v", err)
|
||||
}
|
||||
vault1Metadata, err := vault.LoadVaultMetadata(fs, vault1Dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load vault1 metadata: %v", err)
|
||||
}
|
||||
|
||||
ltIdentity1, err := agehd.DeriveIdentity(testMnemonic, vault1Metadata.DerivationIndex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key for vault1: %v", err)
|
||||
}
|
||||
|
||||
// Load vault2 metadata to get its derivation index
|
||||
vault2Dir, err := vault2.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault2 directory: %v", err)
|
||||
}
|
||||
vault2Metadata, err := vault.LoadVaultMetadata(fs, vault2Dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load vault2 metadata: %v", err)
|
||||
}
|
||||
|
||||
ltIdentity2, err := agehd.DeriveIdentity(testMnemonic, vault2Metadata.DerivationIndex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key for vault2: %v", err)
|
||||
}
|
||||
|
||||
// Unlock the vaults with their respective keys
|
||||
vault1.Unlock(ltIdentity1)
|
||||
vault2.Unlock(ltIdentity2)
|
||||
|
||||
// Add a secret to vault1
|
||||
secretName := "test-secret"
|
||||
secretValue := []byte("secret in vault1")
|
||||
|
||||
secretBuffer := memguard.NewBufferFromBytes(secretValue)
|
||||
defer secretBuffer.Destroy()
|
||||
|
||||
if err := vault1.AddSecret(secretName, secretBuffer, false); err != nil {
|
||||
t.Fatalf("Failed to add secret to vault1: %v", err)
|
||||
}
|
||||
|
||||
// Verify the secret exists in vault1
|
||||
vault1Secrets, err := vault1.ListSecrets()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list secrets in vault1: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, s := range vault1Secrets {
|
||||
if s == secretName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("Secret not found in vault1")
|
||||
}
|
||||
|
||||
// Verify the secret does NOT exist in vault2
|
||||
vault2Secrets, err := vault2.ListSecrets()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list secrets in vault2: %v", err)
|
||||
}
|
||||
|
||||
found = false
|
||||
for _, s := range vault2Secrets {
|
||||
if s == secretName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
t.Errorf("Secret from vault1 should not be visible in vault2")
|
||||
}
|
||||
testVaultIsolation(t, fs, tempDir)
|
||||
})
|
||||
}
|
||||
|
||||
func testCurrentVaultFileHandling(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
t.Helper()
|
||||
|
||||
stateDir := filepath.Join(tempDir, "currentvault-test")
|
||||
|
||||
err := os.MkdirAll(stateDir, 0o700)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a test vault
|
||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
}
|
||||
|
||||
// Get the vault directory
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
|
||||
// Verify the currentvault file exists and contains just the vault name
|
||||
currentVaultPath := filepath.Join(stateDir, "currentvault")
|
||||
|
||||
currentVaultContents, err := os.ReadFile(filepath.Clean(currentVaultPath))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read currentvault file: %v", err)
|
||||
}
|
||||
|
||||
if string(currentVaultContents) != testVaultName {
|
||||
t.Errorf("Expected currentvault to contain %q, got %q",
|
||||
testVaultName, string(currentVaultContents))
|
||||
}
|
||||
|
||||
// Test that ResolveVaultSymlink correctly resolves the path
|
||||
resolvedPath, err := vault.ResolveVaultSymlink(fs, currentVaultPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to resolve currentvault path: %v", err)
|
||||
}
|
||||
|
||||
if resolvedPath != vaultDir {
|
||||
t.Errorf("Expected resolved path to be %s, got %s", vaultDir, resolvedPath)
|
||||
}
|
||||
}
|
||||
|
||||
func testDeepPathSecrets(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
t.Helper()
|
||||
|
||||
stateDir := filepath.Join(tempDir, "deep-path-test")
|
||||
|
||||
err := os.MkdirAll(stateDir, 0o700)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a test vault - CreateVault now handles public key when
|
||||
// mnemonic is in env
|
||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
}
|
||||
|
||||
// Load vault metadata to get its derivation index
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
|
||||
vaultMetadata, err := vault.LoadVaultMetadata(fs, vaultDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load vault metadata: %v", err)
|
||||
}
|
||||
|
||||
// Derive long-term key from mnemonic using the vault's derivation index
|
||||
ltIdentity, err := agehd.DeriveIdentity(testMnemonic,
|
||||
vaultMetadata.DerivationIndex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Unlock the vault
|
||||
vlt.Unlock(ltIdentity)
|
||||
|
||||
// Create a secret with a deeply nested path
|
||||
deepPath := "api/credentials/production/database/primary"
|
||||
secretValue := []byte("supersecretdbpassword")
|
||||
expectedValue := make([]byte, len(secretValue))
|
||||
copy(expectedValue, secretValue)
|
||||
|
||||
secretBuffer := memguard.NewBufferFromBytes(secretValue)
|
||||
defer secretBuffer.Destroy()
|
||||
|
||||
err = vlt.AddSecret(deepPath, secretBuffer, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add secret with deep path: %v", err)
|
||||
}
|
||||
|
||||
// List secrets and verify our deep path secret is there
|
||||
secrets, err := vlt.ListSecrets()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list secrets: %v", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(secrets, deepPath) {
|
||||
t.Errorf("Deep path secret not found in listed secrets")
|
||||
}
|
||||
|
||||
// Retrieve the secret and verify its value
|
||||
retrievedValue, err := vlt.GetSecret(deepPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to retrieve deep path secret: %v", err)
|
||||
}
|
||||
|
||||
if string(retrievedValue) != string(expectedValue) {
|
||||
t.Errorf("Retrieved value doesn't match. Expected %q, got %q",
|
||||
string(expectedValue), string(retrievedValue))
|
||||
}
|
||||
}
|
||||
|
||||
func testKeyCaching(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
t.Helper()
|
||||
|
||||
stateDir := filepath.Join(tempDir, "key-cache-test")
|
||||
|
||||
err := os.MkdirAll(stateDir, 0o700)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a test vault - CreateVault now handles public key when
|
||||
// mnemonic is in env
|
||||
vlt, err := vault.CreateVault(fs, stateDir, testVaultName)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault: %v", err)
|
||||
}
|
||||
|
||||
// Load vault metadata to get its derivation index
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get vault directory: %v", err)
|
||||
}
|
||||
|
||||
vaultMetadata, err := vault.LoadVaultMetadata(fs, vaultDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load vault metadata: %v", err)
|
||||
}
|
||||
|
||||
// Derive long-term key from mnemonic for verification using the
|
||||
// vault's derivation index
|
||||
ltIdentity, err := agehd.DeriveIdentity(testMnemonic,
|
||||
vaultMetadata.DerivationIndex)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Verify the vault is locked initially
|
||||
if !vlt.Locked() {
|
||||
t.Errorf("Vault should be locked initially")
|
||||
}
|
||||
|
||||
// First call to GetOrDeriveLongTermKey should derive and cache the key
|
||||
firstKey, err := vlt.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Verify the vault is now unlocked
|
||||
if vlt.Locked() {
|
||||
t.Errorf("Vault should be unlocked after GetOrDeriveLongTermKey")
|
||||
}
|
||||
|
||||
// Second call should return the cached key without re-deriving
|
||||
secondKey, err := vlt.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get cached long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Verify both keys are the same instance
|
||||
if firstKey != secondKey {
|
||||
t.Errorf("Second key call should return same instance as first call")
|
||||
}
|
||||
|
||||
// Verify the public key matches what we expect
|
||||
expectedPubKey := ltIdentity.Recipient().String()
|
||||
|
||||
actualPubKey := firstKey.Recipient().String()
|
||||
if actualPubKey != expectedPubKey {
|
||||
t.Errorf("Public key mismatch. Expected %s, got %s",
|
||||
expectedPubKey, actualPubKey)
|
||||
}
|
||||
|
||||
// Now clear the key and verify it's locked again
|
||||
vlt.ClearLongTermKey()
|
||||
|
||||
if !vlt.Locked() {
|
||||
t.Errorf("Vault should be locked after clearing key")
|
||||
}
|
||||
|
||||
// Get the key again and verify it works
|
||||
thirdKey, err := vlt.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to re-derive long-term key: %v", err)
|
||||
}
|
||||
|
||||
// Verify the public key still matches
|
||||
actualPubKey = thirdKey.Recipient().String()
|
||||
if actualPubKey != expectedPubKey {
|
||||
t.Errorf("Re-derived public key mismatch. Expected %s, got %s",
|
||||
expectedPubKey, actualPubKey)
|
||||
}
|
||||
}
|
||||
|
||||
func testVaultNameValidation(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
t.Helper()
|
||||
|
||||
stateDir := filepath.Join(tempDir, "name-validation-test")
|
||||
|
||||
err := os.MkdirAll(stateDir, 0o700)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Test valid vault names
|
||||
validNames := []string{
|
||||
"default",
|
||||
"test-vault",
|
||||
"production.vault",
|
||||
"vault_123",
|
||||
"a-very-long-vault-name-with-dashes",
|
||||
}
|
||||
|
||||
for _, name := range validNames {
|
||||
_, err := vault.CreateVault(fs, stateDir, name)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create vault with valid name %q: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test invalid vault names
|
||||
invalidNames := []string{
|
||||
"", // Empty
|
||||
"UPPERCASE", // Uppercase not allowed
|
||||
"invalid/name", // Slashes not allowed in vault names
|
||||
"invalid name", // Spaces not allowed
|
||||
"invalid@name", // Special chars not allowed
|
||||
}
|
||||
|
||||
for _, name := range invalidNames {
|
||||
_, err := vault.CreateVault(fs, stateDir, name)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error creating vault with invalid name %q, "+
|
||||
"but got none", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testMultipleVaults(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
t.Helper()
|
||||
|
||||
stateDir := filepath.Join(tempDir, "multi-vault-test")
|
||||
|
||||
err := os.MkdirAll(stateDir, 0o700)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create three vaults
|
||||
vaultNames := []string{"vault1", "vault2", "vault3"}
|
||||
for _, name := range vaultNames {
|
||||
_, err := vault.CreateVault(fs, stateDir, name)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// List vaults and verify all three are there
|
||||
vaults, err := vault.ListVaults(fs, stateDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list vaults: %v", err)
|
||||
}
|
||||
|
||||
if len(vaults) != 3 {
|
||||
t.Errorf("Expected 3 vaults, got %d", len(vaults))
|
||||
}
|
||||
|
||||
// Test switching between vaults
|
||||
for _, name := range vaultNames {
|
||||
// Select the vault
|
||||
err := vault.SelectVault(fs, stateDir, name)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to select vault %s: %v", name, err)
|
||||
}
|
||||
|
||||
// Get current vault and verify it's the one we selected
|
||||
currentVault, err := vault.GetCurrentVault(fs, stateDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current vault after selecting %s: %v",
|
||||
name, err)
|
||||
}
|
||||
|
||||
if currentVault.GetName() != name {
|
||||
t.Errorf("Expected current vault to be %s, got %s",
|
||||
name, currentVault.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testVaultIsolation(t *testing.T, fs afero.Fs, tempDir string) {
|
||||
t.Helper()
|
||||
|
||||
stateDir := filepath.Join(tempDir, "isolation-test")
|
||||
|
||||
err := os.MkdirAll(stateDir, 0o700)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create state dir: %v", err)
|
||||
}
|
||||
|
||||
// Create two vaults - CreateVault now handles public key when mnemonic
|
||||
// is in env
|
||||
vault1, err := vault.CreateVault(fs, stateDir, "vault1")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault1: %v", err)
|
||||
}
|
||||
|
||||
vault2, err := vault.CreateVault(fs, stateDir, "vault2")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create vault2: %v", err)
|
||||
}
|
||||
|
||||
// Derive long-term keys from mnemonic
|
||||
// Note: Both vaults will have different derivation indexes due to
|
||||
// GetNextDerivationIndex
|
||||
ltIdentity1 := deriveVaultIdentity(t, fs, vault1)
|
||||
ltIdentity2 := deriveVaultIdentity(t, fs, vault2)
|
||||
|
||||
// Unlock the vaults with their respective keys
|
||||
vault1.Unlock(ltIdentity1)
|
||||
vault2.Unlock(ltIdentity2)
|
||||
|
||||
// Add a secret to vault1
|
||||
secretValue := []byte("secret in vault1")
|
||||
|
||||
secretBuffer := memguard.NewBufferFromBytes(secretValue)
|
||||
defer secretBuffer.Destroy()
|
||||
|
||||
err = vault1.AddSecret(testSecretName, secretBuffer, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to add secret to vault1: %v", err)
|
||||
}
|
||||
|
||||
// Verify the secret exists in vault1
|
||||
vault1Secrets, err := vault1.ListSecrets()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list secrets in vault1: %v", err)
|
||||
}
|
||||
|
||||
if !slices.Contains(vault1Secrets, testSecretName) {
|
||||
t.Errorf("Secret not found in vault1")
|
||||
}
|
||||
|
||||
// Verify the secret does NOT exist in vault2
|
||||
vault2Secrets, err := vault2.ListSecrets()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list secrets in vault2: %v", err)
|
||||
}
|
||||
|
||||
if slices.Contains(vault2Secrets, testSecretName) {
|
||||
t.Errorf("Secret from vault1 should not be visible in vault2")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user