check / check (push) Failing after 2s
vault.CreateVault takes the unlocker passphrase and writes the vault directory, its metadata, long-term public key and passphrase unlocker into a temporary directory, renames that into vaults.d once complete, and only then makes the vault current. secret init and secret vault create call it once instead of adding the unlocker afterwards, so a kill part-way leaves either no vault, whose temporary directory the next command that takes the lock deletes, or a complete one. A test records the state directory before every change the call makes and checks each state, and the command run again from it. Model: opus-5-5
358 lines
10 KiB
Go
358 lines
10 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"
|
|
|
|
// 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()
|
|
|
|
// Create vault without a long-term key, which is set up below
|
|
vault, err := CreateVault(fs, testStateDir, "test", nil, nil)
|
|
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
|
|
}
|
|
|
|
func TestVaultAddSecretCreatesVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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)
|
|
|
|
defer retrievedValue.Destroy()
|
|
|
|
assert.Equal(t, expectedValue, retrievedValue.Bytes())
|
|
}
|
|
|
|
func TestVaultAddSecretMultipleVersions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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)
|
|
|
|
defer value.Destroy()
|
|
|
|
assert.Equal(t, []byte("version-2"), value.Bytes())
|
|
}
|
|
|
|
func TestVaultGetSecretVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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
|
|
first, err := vault.GetSecretVersion(testSecretPath, firstVersion)
|
|
require.NoError(t, err)
|
|
|
|
defer first.Destroy()
|
|
|
|
assert.Equal(t, []byte("version-1"), first.Bytes())
|
|
|
|
// Get specific version (second one)
|
|
secondVersion := versions[0] // First in list is most recent
|
|
second, err := vault.GetSecretVersion(testSecretPath, secondVersion)
|
|
require.NoError(t, err)
|
|
|
|
defer second.Destroy()
|
|
|
|
assert.Equal(t, []byte("version-2"), second.Bytes())
|
|
|
|
// An empty version is not one of the versions; GetSecret gets the
|
|
// current one
|
|
_, err = vault.GetSecretVersion(testSecretPath, "")
|
|
require.ErrorIs(t, err, ErrVersionNotFound)
|
|
}
|
|
|
|
func TestVaultVersionTimestamps(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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
|
|
}
|
|
|
|
func TestVaultGetNonExistentVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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")
|
|
}
|
|
|
|
func TestUpdateVersionMetadata(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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())
|
|
}
|