check / check (push) Failing after 35s
version rm, version promote and get --version joined the version argument into a path unchecked, so "", ".", "..", "../../.." removed or read every version, the secret, the vault or directories above it. A version is now accepted only if it is one of the versions ListVersions lists for the secret, compared by name before any path is built (secret.VersionExists, used by all three). An empty --version is rejected instead of meaning the current version: GetSecretVersion no longer treats "" as current, and GetSecret looks the current version up itself. Model: opus-5-5
351 lines
11 KiB
Go
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)
|
|
|
|
// An empty version is not one of the versions; GetSecret gets the
|
|
// current one
|
|
_, err = vault.GetSecretVersion(testSecretPath, "")
|
|
require.ErrorIs(t, err, ErrVersionNotFound)
|
|
}
|
|
|
|
//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())
|
|
}
|