secret/internal/vault/secrets_version_test.go

283 lines
8.6 KiB
Go

package vault
import (
"path/filepath"
"testing"
"time"
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Helper function to create a vault with long-term key set up
func createTestVaultWithKey(t *testing.T, fs afero.Fs, stateDir, vaultName string) *Vault {
// Set mnemonic for testing
t.Setenv(secret.EnvMnemonic, "abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
// Create vault
vault, err := CreateVault(fs, stateDir, vaultName)
require.NoError(t, err)
// Derive and store long-term key from mnemonic
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
ltIdentity, err := agehd.DeriveIdentity(mnemonic, 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()), 0600)
require.NoError(t, err)
// Unlock the vault with the derived key
vault.Unlock(ltIdentity)
return vault
}
func TestVaultAddSecretCreatesVersion(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs, stateDir, "test")
// Add a secret
secretName := "test/secret"
secretValue := []byte("initial-value")
err := vault.AddSecret(secretName, secretValue, false)
require.NoError(t, err)
// 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(secretName)
require.NoError(t, err)
assert.Equal(t, secretValue, retrievedValue)
}
func TestVaultAddSecretMultipleVersions(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs, stateDir, "test")
secretName := "test/secret"
// Add first version
err := vault.AddSecret(secretName, []byte("version-1"), false)
require.NoError(t, err)
// Try to add again without force - should fail
err = vault.AddSecret(secretName, []byte("version-2"), false)
assert.Error(t, err)
assert.Contains(t, err.Error(), "already exists")
// Add with force - should create new version
err = vault.AddSecret(secretName, []byte("version-2"), true)
require.NoError(t, err)
// 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(secretName)
require.NoError(t, err)
assert.Equal(t, []byte("version-2"), value)
}
func TestVaultGetSecretVersion(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs, stateDir, "test")
secretName := "test/secret"
// Add multiple versions
err := vault.AddSecret(secretName, []byte("version-1"), false)
require.NoError(t, err)
// Small delay to ensure different version names
time.Sleep(10 * time.Millisecond)
err = vault.AddSecret(secretName, []byte("version-2"), true)
require.NoError(t, err)
// 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(secretName, 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(secretName, secondVersion)
require.NoError(t, err)
assert.Equal(t, []byte("version-2"), value)
// Get current (empty version)
value, err = vault.GetSecretVersion(secretName, "")
require.NoError(t, err)
assert.Equal(t, []byte("version-2"), value)
}
func TestVaultVersionTimestamps(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs, stateDir, "test")
// Get long-term key
ltIdentity, err := vault.GetOrDeriveLongTermKey()
require.NoError(t, err)
secretName := "test/secret"
// Add first version
beforeFirst := time.Now()
err = vault.AddSecret(secretName, []byte("version-1"), 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.NewSecretVersion(vault, secretName, 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()
err = vault.AddSecret(secretName, []byte("version-2"), true)
require.NoError(t, err)
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.NewSecretVersion(vault, secretName, 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.NewSecretVersion(vault, secretName, 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) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs, stateDir, "test")
// Add a secret
err := vault.AddSecret("test/secret", []byte("value"), false)
require.NoError(t, err)
// Try to get non-existent version
_, err = vault.GetSecretVersion("test/secret", "20991231.999")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestUpdateVersionMetadata(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Create vault with long-term key
vault := createTestVaultWithKey(t, fs, stateDir, "test")
// Get long-term key
ltIdentity, err := vault.GetOrDeriveLongTermKey()
require.NoError(t, err)
// Create a version manually to test updateVersionMetadata
secretName := "test/secret"
versionName := "20231215.001"
version := secret.NewSecretVersion(vault, secretName, versionName)
// Set initial metadata
now := time.Now()
epochPlusOne := time.Unix(1, 0)
version.Metadata.NotBefore = &epochPlusOne
version.Metadata.NotAfter = nil
// Save version
err = version.Save([]byte("test-value"))
require.NoError(t, err)
// Update metadata
version.Metadata.NotAfter = &now
err = updateVersionMetadata(fs, version, ltIdentity)
require.NoError(t, err)
// Load and verify
version2 := secret.NewSecretVersion(vault, secretName, versionName)
err = version2.LoadMetadata(ltIdentity)
require.NoError(t, err)
assert.NotNil(t, version2.Metadata.NotAfter)
assert.Equal(t, now.Unix(), version2.Metadata.NotAfter.Unix())
}