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()) }