package vault_test import ( "os" "path/filepath" "testing" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" ) func TestVaultWithRealFilesystem(t *testing.T) { // Create a temporary directory for our tests tempDir, err := os.MkdirTemp("", "secret-test-") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tempDir) // Clean up after test // Use the real filesystem fs := afero.NewOsFs() // Test mnemonic testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" // Save original environment variables oldMnemonic := os.Getenv(secret.EnvMnemonic) oldPassphrase := os.Getenv(secret.EnvUnlockPassphrase) // Set test environment variables os.Setenv(secret.EnvMnemonic, testMnemonic) os.Setenv(secret.EnvUnlockPassphrase, "test-passphrase") // Clean up after test defer func() { if oldMnemonic != "" { os.Setenv(secret.EnvMnemonic, oldMnemonic) } else { os.Unsetenv(secret.EnvMnemonic) } if oldPassphrase != "" { os.Setenv(secret.EnvUnlockPassphrase, oldPassphrase) } else { os.Unsetenv(secret.EnvUnlockPassphrase) } }() // Test symlink handling t.Run("SymlinkHandling", func(t *testing.T) { stateDir := filepath.Join(tempDir, "symlink-test") if err := os.MkdirAll(stateDir, 0700); err != nil { t.Fatalf("Failed to create state dir: %v", err) } // Create a test vault vlt, err := vault.CreateVault(fs, stateDir, "test-vault") if err != nil { t.Fatalf("Failed to create vault: %v", err) } // Get the vault directory vaultDir, err := vlt.GetDirectory() if err != nil { t.Fatalf("Failed to get vault directory: %v", err) } // Create a symlink to the vault directory in a different location symlinkPath := filepath.Join(tempDir, "test-symlink") if err := os.Symlink(vaultDir, symlinkPath); err != nil { t.Fatalf("Failed to create symlink: %v", err) } // Test that we can resolve the symlink correctly resolvedPath, err := vault.ResolveVaultSymlink(fs, symlinkPath) if err != nil { t.Fatalf("Failed to resolve symlink: %v", err) } // On some platforms, the resolved path might have different case or format // We'll use filepath.EvalSymlinks to get the canonical path for comparison expectedPath, err := filepath.EvalSymlinks(vaultDir) if err != nil { t.Fatalf("Failed to evaluate symlink: %v", err) } actualPath, err := filepath.EvalSymlinks(resolvedPath) if err != nil { t.Fatalf("Failed to evaluate resolved path: %v", err) } if actualPath != expectedPath { t.Errorf("Expected symlink to resolve to %s, got %s", expectedPath, actualPath) } }) // Test secret operations with deeply nested paths t.Run("DeepPathSecrets", func(t *testing.T) { stateDir := filepath.Join(tempDir, "deep-path-test") if err := os.MkdirAll(stateDir, 0700); err != nil { t.Fatalf("Failed to create state dir: %v", err) } // Create a test vault vlt, err := vault.CreateVault(fs, stateDir, "test-vault") if err != nil { t.Fatalf("Failed to create vault: %v", err) } // Derive long-term key from mnemonic ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive long-term key: %v", err) } // Get the vault directory vaultDir, err := vlt.GetDirectory() if err != nil { t.Fatalf("Failed to get vault directory: %v", err) } // Write long-term public key ltPubKeyPath := filepath.Join(vaultDir, "pub.age") pubKey := ltIdentity.Recipient().String() if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil { t.Fatalf("Failed to write long-term public key: %v", err) } // Unlock the vault vlt.Unlock(ltIdentity) // Create a secret with a deeply nested path deepPath := "api/credentials/production/database/primary" secretValue := []byte("supersecretdbpassword") err = vlt.AddSecret(deepPath, secretValue, false) if err != nil { t.Fatalf("Failed to add secret with deep path: %v", err) } // List secrets and verify our deep path secret is there secrets, err := vlt.ListSecrets() if err != nil { t.Fatalf("Failed to list secrets: %v", err) } found := false for _, s := range secrets { if s == deepPath { found = true break } } if !found { t.Errorf("Deep path secret not found in listed secrets") } // Retrieve the secret and verify its value retrievedValue, err := vlt.GetSecret(deepPath) if err != nil { t.Fatalf("Failed to retrieve deep path secret: %v", err) } if string(retrievedValue) != string(secretValue) { t.Errorf("Retrieved value doesn't match. Expected %q, got %q", string(secretValue), string(retrievedValue)) } }) // Test key caching in GetOrDeriveLongTermKey t.Run("KeyCaching", func(t *testing.T) { stateDir := filepath.Join(tempDir, "key-cache-test") if err := os.MkdirAll(stateDir, 0700); err != nil { t.Fatalf("Failed to create state dir: %v", err) } // Create a test vault vlt, err := vault.CreateVault(fs, stateDir, "test-vault") if err != nil { t.Fatalf("Failed to create vault: %v", err) } // Derive long-term key from mnemonic ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive long-term key: %v", err) } // Get the vault directory vaultDir, err := vlt.GetDirectory() if err != nil { t.Fatalf("Failed to get vault directory: %v", err) } // Write long-term public key ltPubKeyPath := filepath.Join(vaultDir, "pub.age") pubKey := ltIdentity.Recipient().String() if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil { t.Fatalf("Failed to write long-term public key: %v", err) } // Verify the vault is locked initially if !vlt.Locked() { t.Errorf("Vault should be locked initially") } // First call to GetOrDeriveLongTermKey should derive and cache the key firstKey, err := vlt.GetOrDeriveLongTermKey() if err != nil { t.Fatalf("Failed to get long-term key: %v", err) } // Verify the vault is now unlocked if vlt.Locked() { t.Errorf("Vault should be unlocked after GetOrDeriveLongTermKey") } // Second call should return the cached key without re-deriving secondKey, err := vlt.GetOrDeriveLongTermKey() if err != nil { t.Fatalf("Failed to get cached long-term key: %v", err) } // Verify both keys are the same instance if firstKey != secondKey { t.Errorf("Second key call should return same instance as first call") } // Verify the public key matches what we expect expectedPubKey := ltIdentity.Recipient().String() actualPubKey := firstKey.Recipient().String() if actualPubKey != expectedPubKey { t.Errorf("Public key mismatch. Expected %s, got %s", expectedPubKey, actualPubKey) } // Now clear the key and verify it's locked again vlt.ClearLongTermKey() if !vlt.Locked() { t.Errorf("Vault should be locked after clearing key") } // Get the key again and verify it works thirdKey, err := vlt.GetOrDeriveLongTermKey() if err != nil { t.Fatalf("Failed to re-derive long-term key: %v", err) } // Verify the public key still matches actualPubKey = thirdKey.Recipient().String() if actualPubKey != expectedPubKey { t.Errorf("Re-derived public key mismatch. Expected %s, got %s", expectedPubKey, actualPubKey) } }) // Test vault name validation t.Run("VaultNameValidation", func(t *testing.T) { stateDir := filepath.Join(tempDir, "name-validation-test") if err := os.MkdirAll(stateDir, 0700); err != nil { t.Fatalf("Failed to create state dir: %v", err) } // Test valid vault names validNames := []string{ "default", "test-vault", "production.vault", "vault_123", "a-very-long-vault-name-with-dashes", } for _, name := range validNames { _, err := vault.CreateVault(fs, stateDir, name) if err != nil { t.Errorf("Failed to create vault with valid name %q: %v", name, err) } } // Test invalid vault names invalidNames := []string{ "", // Empty "UPPERCASE", // Uppercase not allowed "invalid/name", // Slashes not allowed in vault names "invalid name", // Spaces not allowed "invalid@name", // Special chars not allowed } for _, name := range invalidNames { _, err := vault.CreateVault(fs, stateDir, name) if err == nil { t.Errorf("Expected error creating vault with invalid name %q, but got none", name) } } }) // Test multiple vaults and switching between them t.Run("MultipleVaults", func(t *testing.T) { stateDir := filepath.Join(tempDir, "multi-vault-test") if err := os.MkdirAll(stateDir, 0700); err != nil { t.Fatalf("Failed to create state dir: %v", err) } // Create three vaults vaultNames := []string{"vault1", "vault2", "vault3"} for _, name := range vaultNames { _, err := vault.CreateVault(fs, stateDir, name) if err != nil { t.Fatalf("Failed to create vault %s: %v", name, err) } } // List vaults and verify all three are there vaults, err := vault.ListVaults(fs, stateDir) if err != nil { t.Fatalf("Failed to list vaults: %v", err) } if len(vaults) != 3 { t.Errorf("Expected 3 vaults, got %d", len(vaults)) } // Test switching between vaults for _, name := range vaultNames { // Select the vault if err := vault.SelectVault(fs, stateDir, name); err != nil { t.Fatalf("Failed to select vault %s: %v", name, err) } // Get current vault and verify it's the one we selected currentVault, err := vault.GetCurrentVault(fs, stateDir) if err != nil { t.Fatalf("Failed to get current vault after selecting %s: %v", name, err) } if currentVault.GetName() != name { t.Errorf("Expected current vault to be %s, got %s", name, currentVault.GetName()) } } }) // Test adding a secret in one vault and verifying it's not visible in another t.Run("VaultIsolation", func(t *testing.T) { stateDir := filepath.Join(tempDir, "isolation-test") if err := os.MkdirAll(stateDir, 0700); err != nil { t.Fatalf("Failed to create state dir: %v", err) } // Create two vaults vault1, err := vault.CreateVault(fs, stateDir, "vault1") if err != nil { t.Fatalf("Failed to create vault1: %v", err) } vault2, err := vault.CreateVault(fs, stateDir, "vault2") if err != nil { t.Fatalf("Failed to create vault2: %v", err) } // Derive long-term key from mnemonic ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0) if err != nil { t.Fatalf("Failed to derive long-term key: %v", err) } // Setup both vaults with the same long-term key for _, vlt := range []*vault.Vault{vault1, vault2} { vaultDir, err := vlt.GetDirectory() if err != nil { t.Fatalf("Failed to get vault directory: %v", err) } ltPubKeyPath := filepath.Join(vaultDir, "pub.age") pubKey := ltIdentity.Recipient().String() if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil { t.Fatalf("Failed to write long-term public key: %v", err) } vlt.Unlock(ltIdentity) } // Add a secret to vault1 secretName := "test-secret" secretValue := []byte("secret in vault1") if err := vault1.AddSecret(secretName, secretValue, false); err != nil { t.Fatalf("Failed to add secret to vault1: %v", err) } // Verify the secret exists in vault1 vault1Secrets, err := vault1.ListSecrets() if err != nil { t.Fatalf("Failed to list secrets in vault1: %v", err) } found := false for _, s := range vault1Secrets { if s == secretName { found = true break } } if !found { t.Errorf("Secret not found in vault1") } // Verify the secret does NOT exist in vault2 vault2Secrets, err := vault2.ListSecrets() if err != nil { t.Fatalf("Failed to list secrets in vault2: %v", err) } found = false for _, s := range vault2Secrets { if s == secretName { found = true break } } if found { t.Errorf("Secret from vault1 should not be visible in vault2") } }) }