Files
secret/internal/vault/secrets_version_test.go
sneak 9ee216f629
All checks were successful
check / check (push) Successful in 1m8s
Update golangci-lint to v2.12.2 with canonical config
- Replace .golangci.yml with the canonical strict config (all linters
  enabled except the standard disable list; lll 88, funlen 80/50,
  cyclop 15, dupl 100; test files now linted)
- Pin the Dockerfile lint stage to golangci/golangci-lint:v2.12.2 by
  tag and digest (Debian-based)
- Fix all ~1550 findings surfaced by the new config: line wrapping,
  wsl_v5/nlreturn blank lines, noinlineerr splits, err113 sentinel
  errors, perfsprint/modernize rewrites, goconst constants, thelper,
  testifylint, noctx CommandContext, testpackage conversions,
  t.Parallel() where safe, and complexity/dupl helper extraction
- Record the change and follow-up items in TODO.md
2026-08-07 17:27:23 +00:00

351 lines
11 KiB
Go

// Vault-Level Version Operation Tests
//
// Integration tests for vault-level version operations:
//
// - TestVaultAddSecretCreatesVersion: Tests that AddSecret creates proper
// version structure
// - TestVaultAddSecretMultipleVersions: Tests creating multiple versions with
// force flag
// - TestVaultGetSecretVersion: Tests retrieving specific versions and current
// version
// - TestVaultVersionTimestamps: Tests timestamp logic (notBefore/notAfter)
// across versions
// - TestVaultGetNonExistentVersion: Tests error handling for invalid versions
// - TestUpdateVersionMetadata: Tests metadata update functionality
//
// Version Management:
// - List versions in reverse chronological order
// - Promote any version to current
// - Promotion doesn't modify timestamps
// - Metadata remains encrypted and intact
//nolint:testpackage // white-box test of unexported updateVersionMetadata
package vault
import (
"path/filepath"
"testing"
"time"
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testMnemonic is the mnemonic used to derive the vault long-term key.
//
//nolint:dupword // BIP39 test mnemonic intentionally repeats a word
const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon about"
// envTestMnemonic is the (deliberately different) mnemonic placed in the
// environment; the vault is unlocked manually with the derived key in
// createTestVaultWithKey.
//
//nolint:dupword // BIP39-style test mnemonic intentionally repeats a word
const envTestMnemonic = "abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon about"
// Shared fixtures for white-box tests in this package.
const (
testStateDir = "/test/state"
testSecretPath = "test/secret"
)
// Helper function to add a secret to vault with proper buffer protection
func addTestSecretToVault(
t *testing.T, vault *Vault, name string, value []byte, force bool,
) {
t.Helper()
buffer := memguard.NewBufferFromBytes(value)
defer buffer.Destroy()
err := vault.AddSecret(name, buffer, force)
require.NoError(t, err)
}
// Helper function to create a vault named "test" with its long-term key set
// up and unlocked
func createTestVaultWithKey(t *testing.T, fs afero.Fs) *Vault {
t.Helper()
// Set mnemonic for testing
t.Setenv(secret.EnvMnemonic, envTestMnemonic)
// Create vault
vault, err := CreateVault(fs, testStateDir, "test")
require.NoError(t, err)
// Derive and store long-term key from mnemonic
ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
require.NoError(t, err)
// Store long-term public key in vault
vaultDir, _ := vault.GetDirectory()
ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
err = afero.WriteFile(fs, ltPubKeyPath,
[]byte(ltIdentity.Recipient().String()), 0o600)
require.NoError(t, err)
// Unlock the vault with the derived key
vault.Unlock(ltIdentity)
return vault
}
//nolint:paralleltest // createTestVaultWithKey uses t.Setenv
func TestVaultAddSecretCreatesVersion(t *testing.T) {
fs := afero.NewMemMapFs()
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs)
// Add a secret
secretValue := []byte("initial-value")
expectedValue := make([]byte, len(secretValue))
copy(expectedValue, secretValue)
addTestSecretToVault(t, vault, testSecretPath, secretValue, false)
// Check that version directory was created
vaultDir, _ := vault.GetDirectory()
secretDir := vaultDir + "/secrets.d/test%secret"
versionsDir := secretDir + "/versions"
// Should have one version
entries, err := afero.ReadDir(fs, versionsDir)
require.NoError(t, err)
assert.Len(t, entries, 1)
// Should have current symlink
currentPath := secretDir + "/current"
exists, err := afero.Exists(fs, currentPath)
require.NoError(t, err)
assert.True(t, exists)
// Get the secret value
retrievedValue, err := vault.GetSecret(testSecretPath)
require.NoError(t, err)
assert.Equal(t, expectedValue, retrievedValue)
}
//nolint:paralleltest // createTestVaultWithKey uses t.Setenv
func TestVaultAddSecretMultipleVersions(t *testing.T) {
fs := afero.NewMemMapFs()
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs)
// Add first version
addTestSecretToVault(t, vault, testSecretPath, []byte("version-1"), false)
// Try to add again without force - should fail
failBuffer := memguard.NewBufferFromBytes([]byte("version-2"))
defer failBuffer.Destroy()
err := vault.AddSecret(testSecretPath, failBuffer, false)
require.Error(t, err)
assert.Contains(t, err.Error(), "already exists")
// Add with force - should create new version
addTestSecretToVault(t, vault, testSecretPath, []byte("version-2"), true)
// Check that we have two versions
vaultDir, _ := vault.GetDirectory()
versionsDir := vaultDir + "/secrets.d/test%secret/versions"
entries, err := afero.ReadDir(fs, versionsDir)
require.NoError(t, err)
assert.Len(t, entries, 2)
// Current value should be version-2
value, err := vault.GetSecret(testSecretPath)
require.NoError(t, err)
assert.Equal(t, []byte("version-2"), value)
}
//nolint:paralleltest // createTestVaultWithKey uses t.Setenv
func TestVaultGetSecretVersion(t *testing.T) {
fs := afero.NewMemMapFs()
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs)
// Add multiple versions
addTestSecretToVault(t, vault, testSecretPath, []byte("version-1"), false)
// Small delay to ensure different version names
time.Sleep(10 * time.Millisecond)
addTestSecretToVault(t, vault, testSecretPath, []byte("version-2"), true)
// Get versions list
vaultDir, _ := vault.GetDirectory()
secretDir := vaultDir + "/secrets.d/test%secret"
versions, err := secret.ListVersions(fs, secretDir)
require.NoError(t, err)
require.Len(t, versions, 2)
// Get specific version (first one)
firstVersion := versions[1] // Last in list is first created
value, err := vault.GetSecretVersion(testSecretPath, firstVersion)
require.NoError(t, err)
assert.Equal(t, []byte("version-1"), value)
// Get specific version (second one)
secondVersion := versions[0] // First in list is most recent
value, err = vault.GetSecretVersion(testSecretPath, secondVersion)
require.NoError(t, err)
assert.Equal(t, []byte("version-2"), value)
// Get current (empty version)
value, err = vault.GetSecretVersion(testSecretPath, "")
require.NoError(t, err)
assert.Equal(t, []byte("version-2"), value)
}
//nolint:paralleltest // createTestVaultWithKey uses t.Setenv
func TestVaultVersionTimestamps(t *testing.T) {
fs := afero.NewMemMapFs()
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs)
// Get long-term key
ltIdentity, err := vault.GetOrDeriveLongTermKey()
require.NoError(t, err)
// Add first version
beforeFirst := time.Now()
v1Buffer := memguard.NewBufferFromBytes([]byte("version-1"))
defer v1Buffer.Destroy()
err = vault.AddSecret(testSecretPath, v1Buffer, false)
require.NoError(t, err)
afterFirst := time.Now()
// Get first version metadata
vaultDir, _ := vault.GetDirectory()
secretDir := vaultDir + "/secrets.d/test%secret"
versions, err := secret.ListVersions(fs, secretDir)
require.NoError(t, err)
require.Len(t, versions, 1)
firstVersion := secret.NewVersion(vault, testSecretPath, versions[0])
err = firstVersion.LoadMetadata(ltIdentity)
require.NoError(t, err)
// Check first version timestamps
assert.NotNil(t, firstVersion.Metadata.CreatedAt)
assert.True(t,
firstVersion.Metadata.CreatedAt.After(beforeFirst.Add(-time.Second)))
assert.True(t,
firstVersion.Metadata.CreatedAt.Before(afterFirst.Add(time.Second)))
assert.NotNil(t, firstVersion.Metadata.NotBefore)
assert.Equal(t, int64(1), firstVersion.Metadata.NotBefore.Unix()) // Epoch + 1
assert.Nil(t, firstVersion.Metadata.NotAfter) // Still current
// Add second version
time.Sleep(10 * time.Millisecond)
beforeSecond := time.Now()
addTestSecretToVault(t, vault, testSecretPath, []byte("version-2"), true)
afterSecond := time.Now()
// Get updated versions
versions, err = secret.ListVersions(fs, secretDir)
require.NoError(t, err)
require.Len(t, versions, 2)
// Reload first version metadata (should have notAfter now)
firstVersion = secret.NewVersion(vault, testSecretPath, versions[1])
err = firstVersion.LoadMetadata(ltIdentity)
require.NoError(t, err)
assert.NotNil(t, firstVersion.Metadata.NotAfter)
assert.True(t,
firstVersion.Metadata.NotAfter.After(beforeSecond.Add(-time.Second)))
assert.True(t,
firstVersion.Metadata.NotAfter.Before(afterSecond.Add(time.Second)))
// Check second version timestamps
secondVersion := secret.NewVersion(vault, testSecretPath, versions[0])
err = secondVersion.LoadMetadata(ltIdentity)
require.NoError(t, err)
assert.NotNil(t, secondVersion.Metadata.NotBefore)
assert.True(t,
secondVersion.Metadata.NotBefore.After(beforeSecond.Add(-time.Second)))
assert.True(t,
secondVersion.Metadata.NotBefore.Before(afterSecond.Add(time.Second)))
assert.Nil(t, secondVersion.Metadata.NotAfter) // Current version
}
//nolint:paralleltest // createTestVaultWithKey uses t.Setenv
func TestVaultGetNonExistentVersion(t *testing.T) {
fs := afero.NewMemMapFs()
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs)
// Add a secret
addTestSecretToVault(t, vault, testSecretPath, []byte("value"), false)
// Try to get non-existent version
_, err := vault.GetSecretVersion(testSecretPath, "20991231.999")
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
//nolint:paralleltest // createTestVaultWithKey uses t.Setenv
func TestUpdateVersionMetadata(t *testing.T) {
fs := afero.NewMemMapFs()
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs)
// Get long-term key
ltIdentity, err := vault.GetOrDeriveLongTermKey()
require.NoError(t, err)
// Create a version manually to test updateVersionMetadata
versionName := "20231215.001"
version := secret.NewVersion(vault, testSecretPath, versionName)
// Set initial metadata
now := time.Now()
epochPlusOne := time.Unix(1, 0)
version.Metadata.NotBefore = &epochPlusOne
version.Metadata.NotAfter = nil
// Save version
testBuffer := memguard.NewBufferFromBytes([]byte("test-value"))
defer testBuffer.Destroy()
err = version.Save(testBuffer)
require.NoError(t, err)
// Update metadata
version.Metadata.NotAfter = &now
err = updateVersionMetadata(fs, version, ltIdentity)
require.NoError(t, err)
// Load and verify
version2 := secret.NewVersion(vault, testSecretPath, versionName)
err = version2.LoadMetadata(ltIdentity)
require.NoError(t, err)
assert.NotNil(t, version2.Metadata.NotAfter)
assert.Equal(t, now.Unix(), version2.Metadata.NotAfter.Unix())
}