// Version Support Test Suite Documentation // // This file contains core unit tests for version functionality: // // - TestGenerateVersionName: Tests version name generation with date and serial format // - TestGenerateVersionNameMaxSerial: Tests the 999 versions per day limit // - TestNewVersion: Tests secret version object creation // - TestSecretVersionSave: Tests saving a version with encryption // - TestSecretVersionLoadMetadata: Tests loading and decrypting version metadata // - TestSecretVersionGetValue: Tests retrieving and decrypting version values // - TestListVersions: Tests listing versions in reverse chronological order // - TestGetCurrentVersion: Tests retrieving the current version via symlink // - TestSetCurrentVersion: Tests updating the current version symlink // - TestVersionMetadataTimestamps: Tests timestamp pointer consistency // // Key Test Scenarios: // - Version Creation: First version gets notBefore = epoch + 1 second // - Subsequent versions update previous version's notAfter timestamp // - New version's notBefore equals previous version's notAfter // - Version names follow YYYYMMDD.NNN format // - Maximum 999 versions per day enforced // // Version Retrieval: // - Get current version via symlink // - Get specific version by name // - Empty version parameter returns current // - Non-existent versions return appropriate errors // // Data Integrity: // - Each version has independent encryption keys // - Metadata encryption protects version history // - Long-term key required for all operations // - Concurrent reads handled safely package secret_test import ( "errors" "path/filepath" "testing" "time" "filippo.io/age" "github.com/awnumar/memguard" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/secret/internal/secret" ) const ( testSecretDir = "/test/secret" testVaultName = "test" testVaultStateDir = "/test" ) var errNotImplementedInMock = errors.New("not implemented in mock") // MockVersionVault implements VaultInterface for testing type MockVersionVault struct { Name string fs afero.Fs stateDir string longTermKey *age.X25519Identity } func (m *MockVersionVault) GetDirectory() (string, error) { return filepath.Join(m.stateDir, "vaults.d", m.Name), nil } func (m *MockVersionVault) AddSecret(_ string, _ *memguard.LockedBuffer, _ bool) error { return errNotImplementedInMock } func (m *MockVersionVault) GetName() string { return m.Name } //nolint:ireturn // implements VaultInterface func (m *MockVersionVault) GetFilesystem() afero.Fs { return m.fs } //nolint:ireturn // implements VaultInterface func (m *MockVersionVault) GetCurrentUnlocker() (secret.Unlocker, error) { return nil, errNotImplementedInMock } func (m *MockVersionVault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) { return nil, errNotImplementedInMock } func (m *MockVersionVault) SetMnemonic(_ *memguard.LockedBuffer) {} func (m *MockVersionVault) SetUnlockPassphrase(_ *memguard.LockedBuffer) {} func (m *MockVersionVault) CreatePassphraseUnlocker( _ *memguard.LockedBuffer, ) (*secret.PassphraseUnlocker, error) { return nil, errNotImplementedInMock } func TestGenerateVersionName(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() secretDir := testSecretDir // Test first version generation version1, err := secret.GenerateVersionName(fs, secretDir) require.NoError(t, err) assert.Regexp(t, `^\d{8}\.001$`, version1) // Create the version directory versionDir := filepath.Join(secretDir, "versions", version1) err = fs.MkdirAll(versionDir, 0o755) require.NoError(t, err) // Test second version generation on same day version2, err := secret.GenerateVersionName(fs, secretDir) require.NoError(t, err) assert.Regexp(t, `^\d{8}\.002$`, version2) // Verify they have the same date prefix assert.Equal(t, version1[:8], version2[:8]) assert.NotEqual(t, version1, version2) } func TestNewVersion(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() vault := &MockVersionVault{ Name: testVaultName, fs: fs, stateDir: testVaultStateDir, } sv := secret.NewVersion(vault, "test/secret", "20231215.001") assert.Equal(t, "test/secret", sv.SecretName) assert.Equal(t, "20231215.001", sv.Version) assert.Contains(t, sv.Directory, "test%secret/versions/20231215.001") assert.NotEmpty(t, sv.Metadata.ID) assert.NotNil(t, sv.Metadata.CreatedAt) } func TestSecretVersionSave(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() vault := &MockVersionVault{ Name: testVaultName, fs: fs, stateDir: testVaultStateDir, } // Create vault directory structure and long-term key vaultDir, _ := vault.GetDirectory() err := fs.MkdirAll(vaultDir, 0o755) require.NoError(t, err) // Generate and store long-term public key ltIdentity, err := age.GenerateX25519Identity() require.NoError(t, err) vault.longTermKey = ltIdentity ltPubKeyPath := filepath.Join(vaultDir, "pub.age") err = afero.WriteFile( fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600) require.NoError(t, err) // Create and save a version sv := secret.NewVersion(vault, "test/secret", "20231215.001") testValue := []byte("test-secret-value") testBuffer := memguard.NewBufferFromBytes(testValue) defer testBuffer.Destroy() err = sv.Save(testBuffer) require.NoError(t, err) // Verify files were created assert.True(t, fileExists(fs, filepath.Join(sv.Directory, "pub.age"))) assert.True(t, fileExists(fs, filepath.Join(sv.Directory, "priv.age"))) assert.True(t, fileExists(fs, filepath.Join(sv.Directory, "value.age"))) assert.True(t, fileExists(fs, filepath.Join(sv.Directory, "metadata.age"))) } func TestSecretVersionLoadMetadata(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() vault := &MockVersionVault{ Name: testVaultName, fs: fs, stateDir: testVaultStateDir, } // Setup vault with long-term key vaultDir, _ := vault.GetDirectory() err := fs.MkdirAll(vaultDir, 0o755) require.NoError(t, err) ltIdentity, err := age.GenerateX25519Identity() require.NoError(t, err) vault.longTermKey = ltIdentity ltPubKeyPath := filepath.Join(vaultDir, "pub.age") err = afero.WriteFile( fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600) require.NoError(t, err) // Create and save a version with custom metadata sv := secret.NewVersion(vault, "test/secret", "20231215.001") now := time.Now() epochPlusOne := time.Unix(1, 0) sv.Metadata.NotBefore = &epochPlusOne sv.Metadata.NotAfter = &now testBuffer := memguard.NewBufferFromBytes([]byte("test-value")) defer testBuffer.Destroy() err = sv.Save(testBuffer) require.NoError(t, err) // Create new version object and load metadata sv2 := secret.NewVersion(vault, "test/secret", "20231215.001") err = sv2.LoadMetadata(ltIdentity) require.NoError(t, err) // Verify loaded metadata assert.Equal(t, sv.Metadata.ID, sv2.Metadata.ID) assert.NotNil(t, sv2.Metadata.NotBefore) assert.Equal(t, epochPlusOne.Unix(), sv2.Metadata.NotBefore.Unix()) assert.NotNil(t, sv2.Metadata.NotAfter) } func TestSecretVersionGetValue(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() vault := &MockVersionVault{ Name: testVaultName, fs: fs, stateDir: testVaultStateDir, } // Setup vault with long-term key vaultDir, _ := vault.GetDirectory() err := fs.MkdirAll(vaultDir, 0o755) require.NoError(t, err) ltIdentity, err := age.GenerateX25519Identity() require.NoError(t, err) vault.longTermKey = ltIdentity ltPubKeyPath := filepath.Join(vaultDir, "pub.age") err = afero.WriteFile( fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600) require.NoError(t, err) // Create and save a version sv := secret.NewVersion(vault, "test/secret", "20231215.001") originalValue := []byte("test-secret-value-12345") expectedValue := make([]byte, len(originalValue)) copy(expectedValue, originalValue) originalBuffer := memguard.NewBufferFromBytes(originalValue) defer originalBuffer.Destroy() err = sv.Save(originalBuffer) require.NoError(t, err) // Retrieve the value retrievedBuffer, err := sv.GetValue(ltIdentity) require.NoError(t, err) defer retrievedBuffer.Destroy() assert.Equal(t, expectedValue, retrievedBuffer.Bytes()) } func TestListVersions(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() secretDir := testSecretDir versionsDir := filepath.Join(secretDir, "versions") // No versions directory versions, err := secret.ListVersions(fs, secretDir) require.NoError(t, err) assert.Empty(t, versions) // Create some versions testVersions := []string{ "20231215.001", "20231215.002", "20231216.001", "20231214.001", } for _, v := range testVersions { err := fs.MkdirAll(filepath.Join(versionsDir, v), 0o755) require.NoError(t, err) } // Create a file (not directory) that should be ignored err = afero.WriteFile( fs, filepath.Join(versionsDir, "ignore.txt"), []byte("test"), 0o600) require.NoError(t, err) // List versions versions, err = secret.ListVersions(fs, secretDir) require.NoError(t, err) // Should be sorted in reverse chronological order expected := []string{ "20231216.001", "20231215.002", "20231215.001", "20231214.001", } assert.Equal(t, expected, versions) } func TestGetCurrentVersion(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() secretDir := testSecretDir // The current file contains just the version name currentPath := filepath.Join(secretDir, "current") err := fs.MkdirAll(secretDir, 0o755) require.NoError(t, err) err = afero.WriteFile(fs, currentPath, []byte("20231216.001"), 0o600) require.NoError(t, err) version, err := secret.GetCurrentVersion(fs, secretDir) require.NoError(t, err) assert.Equal(t, "20231216.001", version) } func TestSetCurrentVersion(t *testing.T) { t.Parallel() fs := afero.NewMemMapFs() secretDir := testSecretDir err := fs.MkdirAll(secretDir, 0o755) require.NoError(t, err) // Set current version err = secret.SetCurrentVersion(fs, secretDir, "20231216.002") require.NoError(t, err) // Verify it was set version, err := secret.GetCurrentVersion(fs, secretDir) require.NoError(t, err) assert.Equal(t, "20231216.002", version) // Update to different version err = secret.SetCurrentVersion(fs, secretDir, "20231217.001") require.NoError(t, err) version, err = secret.GetCurrentVersion(fs, secretDir) require.NoError(t, err) assert.Equal(t, "20231217.001", version) } func TestVersionMetadataTimestamps(t *testing.T) { t.Parallel() // Test that all timestamp fields behave consistently as pointers vm := secret.VersionMetadata{ ID: "test-id", } // All should be nil initially assert.Nil(t, vm.CreatedAt) assert.Nil(t, vm.NotBefore) assert.Nil(t, vm.NotAfter) // Set timestamps now := time.Now() epoch := time.Unix(1, 0) future := now.Add(time.Hour) vm.CreatedAt = &now vm.NotBefore = &epoch vm.NotAfter = &future // All should be non-nil assert.NotNil(t, vm.CreatedAt) assert.NotNil(t, vm.NotBefore) assert.NotNil(t, vm.NotAfter) // Values should match assert.Equal(t, now.Unix(), vm.CreatedAt.Unix()) assert.Equal(t, int64(1), vm.NotBefore.Unix()) assert.Equal(t, future.Unix(), vm.NotAfter.Unix()) } // Helper function func fileExists(fs afero.Fs, path string) bool { exists, _ := afero.Exists(fs, path) return exists }