latest from ai, it broke the tests
This commit is contained in:
@@ -6,7 +6,7 @@ import (
|
||||
"math/big"
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"git.eeqj.de/sneak/secret/internal/vault"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
@@ -31,7 +31,7 @@ func newGenerateMnemonicCmd() *cobra.Command {
|
||||
Long: `Generate a cryptographically secure random BIP39 mnemonic phrase that can be used with 'secret init' or 'secret import'.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli := NewCLIInstance()
|
||||
return cli.GenerateMnemonic()
|
||||
return cli.GenerateMnemonic(cmd)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ func newGenerateSecretCmd() *cobra.Command {
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
cli := NewCLIInstance()
|
||||
return cli.GenerateSecret(args[0], length, secretType, force)
|
||||
return cli.GenerateSecret(cmd, args[0], length, secretType, force)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func newGenerateSecretCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
// GenerateMnemonic generates a random BIP39 mnemonic phrase
|
||||
func (cli *CLIInstance) GenerateMnemonic() error {
|
||||
func (cli *CLIInstance) GenerateMnemonic(cmd *cobra.Command) error {
|
||||
// Generate 128 bits of entropy for a 12-word mnemonic
|
||||
entropy, err := bip39.NewEntropy(128)
|
||||
if err != nil {
|
||||
@@ -74,7 +74,7 @@ func (cli *CLIInstance) GenerateMnemonic() error {
|
||||
}
|
||||
|
||||
// Output mnemonic to stdout
|
||||
fmt.Println(mnemonic)
|
||||
cmd.Println(mnemonic)
|
||||
|
||||
// Output helpful information to stderr
|
||||
fmt.Fprintln(os.Stderr, "")
|
||||
@@ -92,7 +92,7 @@ func (cli *CLIInstance) GenerateMnemonic() error {
|
||||
}
|
||||
|
||||
// GenerateSecret generates a random secret and stores it in the vault
|
||||
func (cli *CLIInstance) GenerateSecret(secretName string, length int, secretType string, force bool) error {
|
||||
func (cli *CLIInstance) GenerateSecret(cmd *cobra.Command, secretName string, length int, secretType string, force bool) error {
|
||||
if length < 1 {
|
||||
return fmt.Errorf("length must be at least 1")
|
||||
}
|
||||
@@ -116,16 +116,16 @@ func (cli *CLIInstance) GenerateSecret(secretName string, length int, secretType
|
||||
}
|
||||
|
||||
// Store the secret in the vault
|
||||
vault, err := secret.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := vault.AddSecret(secretName, []byte(secretValue), force); err != nil {
|
||||
if err := vlt.AddSecret(secretName, []byte(secretValue), force); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Generated and stored %d-character %s secret: %s\n", length, secretType, secretName)
|
||||
cmd.Printf("Generated and stored %d-character %s secret: %s\n", length, secretType, secretName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,19 +16,23 @@ import (
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
|
||||
func newInitCmd() *cobra.Command {
|
||||
// NewInitCmd creates the init command
|
||||
func NewInitCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize the secrets manager",
|
||||
Long: `Create the necessary directory structure for storing secrets and generate encryption keys.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli := NewCLIInstance()
|
||||
return cli.Init(cmd)
|
||||
},
|
||||
RunE: RunInit,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the secrets manager
|
||||
// RunInit is the exported function that handles the init command
|
||||
func RunInit(cmd *cobra.Command, args []string) error {
|
||||
cli := NewCLIInstance()
|
||||
return cli.Init(cmd)
|
||||
}
|
||||
|
||||
// Init initializes the secret manager
|
||||
func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
||||
secret.Debug("Starting secret manager initialization")
|
||||
|
||||
|
||||
@@ -10,15 +10,50 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/cli"
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMain runs before all tests and ensures the binary is built
|
||||
func TestMain(m *testing.M) {
|
||||
// Get the current working directory
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to get working directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Navigate up from internal/cli to project root
|
||||
projectRoot := filepath.Join(wd, "..", "..")
|
||||
|
||||
// Build the binary
|
||||
cmd := exec.Command("go", "build", "-o", "secret", "./cmd/secret")
|
||||
cmd.Dir = projectRoot
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to build secret binary: %v\nOutput: %s\n", err, output)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Run the tests
|
||||
code := m.Run()
|
||||
|
||||
// Clean up the binary
|
||||
os.Remove(filepath.Join(projectRoot, "secret"))
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// TestSecretManagerIntegration is a comprehensive integration test that exercises
|
||||
// all functionality of the secret manager using a real filesystem in a temporary directory.
|
||||
// This test serves as both validation and documentation of the program's behavior.
|
||||
func TestSecretManagerIntegration(t *testing.T) {
|
||||
// Enable debug logging to diagnose test failures
|
||||
os.Setenv("GODEBUG", "berlin.sneak.pkg.secret")
|
||||
defer os.Unsetenv("GODEBUG")
|
||||
|
||||
// Test configuration
|
||||
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
testPassphrase := "test-passphrase-123"
|
||||
@@ -30,48 +65,30 @@ func TestSecretManagerIntegration(t *testing.T) {
|
||||
os.Setenv("SB_SECRET_STATE_DIR", tempDir)
|
||||
defer os.Unsetenv("SB_SECRET_STATE_DIR")
|
||||
|
||||
// Find the secret binary path
|
||||
// Look for it relative to the test file location
|
||||
// Find the secret binary path (needed for tests that still use exec.Command)
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err, "should get working directory")
|
||||
|
||||
// Navigate up from internal/cli to project root
|
||||
projectRoot := filepath.Join(wd, "..", "..")
|
||||
secretPath := filepath.Join(projectRoot, "secret")
|
||||
|
||||
// Verify the binary exists
|
||||
_, err = os.Stat(secretPath)
|
||||
require.NoError(t, err, "secret binary should exist at %s", secretPath)
|
||||
|
||||
// Helper function to run the secret command
|
||||
runSecret := func(args ...string) (string, error) {
|
||||
cmd := exec.Command(secretPath, args...)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
return string(output), err
|
||||
return cli.ExecuteCommandInProcess(args, "", nil)
|
||||
}
|
||||
|
||||
// Helper function to run secret with environment variables
|
||||
runSecretWithEnv := func(env map[string]string, args ...string) (string, error) {
|
||||
cmd := exec.Command(secretPath, args...)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
for k, v := range env {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
return string(output), err
|
||||
return cli.ExecuteCommandInProcess(args, "", env)
|
||||
}
|
||||
|
||||
// Helper function to run secret with stdin
|
||||
runSecretWithStdin := func(stdin string, env map[string]string, args ...string) (string, error) {
|
||||
return cli.ExecuteCommandInProcess(args, stdin, env)
|
||||
}
|
||||
|
||||
// Declare runSecret to avoid unused variable error - will be used in later tests
|
||||
_ = runSecret
|
||||
_ = runSecretWithStdin
|
||||
|
||||
// Test 1: Initialize secret manager
|
||||
// Command: secret init
|
||||
@@ -81,7 +98,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
||||
// - currentvault symlink -> vaults.d/default
|
||||
// - default vault has pub.age file
|
||||
// - default vault has unlockers.d directory with passphrase unlocker
|
||||
test01Initialize(t, tempDir, secretPath, testMnemonic, testPassphrase, runSecretWithEnv)
|
||||
test01Initialize(t, tempDir, testMnemonic, testPassphrase, runSecretWithEnv)
|
||||
|
||||
// Test 2: Vault management - List vaults
|
||||
// Command: secret vault list
|
||||
@@ -113,7 +130,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
||||
// - secrets.d/database%password/versions/YYYYMMDD.001/ created
|
||||
// - Version directory contains: pub.age, priv.age, value.age, metadata.age
|
||||
// - current symlink points to version directory
|
||||
test05AddSecret(t, tempDir, secretPath, testMnemonic, runSecret, runSecretWithEnv)
|
||||
test05AddSecret(t, tempDir, testMnemonic, runSecret, runSecretWithEnv, runSecretWithStdin)
|
||||
|
||||
// Test 6: Retrieve secret
|
||||
// Command: secret get database/password
|
||||
@@ -128,7 +145,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
||||
// - New version directory YYYYMMDD.002 created
|
||||
// - current symlink updated to new version
|
||||
// - Old version still exists
|
||||
test07AddSecretVersion(t, tempDir, secretPath, testMnemonic, runSecret, runSecretWithEnv)
|
||||
test07AddSecretVersion(t, tempDir, testMnemonic, runSecret, runSecretWithEnv, runSecretWithStdin)
|
||||
|
||||
// Test 8: List secret versions
|
||||
// Command: secret version list database/password
|
||||
@@ -154,13 +171,13 @@ func TestSecretManagerIntegration(t *testing.T) {
|
||||
// Command: secret list
|
||||
// Purpose: Show all secrets in current vault
|
||||
// Expected: Shows database/password with metadata
|
||||
test11ListSecrets(t, tempDir, secretPath, testMnemonic, runSecret)
|
||||
test11ListSecrets(t, tempDir, testMnemonic, runSecret, runSecretWithStdin)
|
||||
|
||||
// Test 12: Add secrets with different name formats
|
||||
// Commands: Various secret names (paths, dots, underscores)
|
||||
// Purpose: Test secret name validation and storage encoding
|
||||
// Expected: Proper filesystem encoding (/ -> %)
|
||||
test12SecretNameFormats(t, tempDir, secretPath, testMnemonic, runSecretWithEnv)
|
||||
test12SecretNameFormats(t, tempDir, testMnemonic, runSecretWithEnv, runSecretWithStdin)
|
||||
|
||||
// Test 13: Unlocker management
|
||||
// Commands: secret unlockers list, secret unlockers add pgp
|
||||
@@ -180,7 +197,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
||||
// Test 15: Cross-vault isolation
|
||||
// Purpose: Verify secrets in one vault aren't accessible from another
|
||||
// Expected: Secrets from work vault not visible in default vault
|
||||
test15VaultIsolation(t, tempDir, secretPath, testMnemonic, runSecret, runSecretWithEnv)
|
||||
test15VaultIsolation(t, tempDir, testMnemonic, runSecret, runSecretWithEnv, runSecretWithStdin)
|
||||
|
||||
// Test 16: Generate random secrets
|
||||
// Command: secret generate secret api/key --length 32 --type base58
|
||||
@@ -192,7 +209,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
||||
// Command: secret import ssh/key --source ~/.ssh/id_rsa
|
||||
// Purpose: Import existing file as secret
|
||||
// Expected: File contents stored as secret value
|
||||
test17ImportFromFile(t, tempDir, secretPath, testMnemonic, runSecretWithEnv)
|
||||
test17ImportFromFile(t, tempDir, testMnemonic, runSecretWithEnv, runSecretWithStdin)
|
||||
|
||||
// Test 18: Age key management
|
||||
// Commands: secret encrypt/decrypt using stored age keys
|
||||
@@ -277,7 +294,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
||||
|
||||
// Helper functions for each test section
|
||||
|
||||
func test01Initialize(t *testing.T, tempDir, secretPath, testMnemonic, testPassphrase string, runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
func test01Initialize(t *testing.T, tempDir, testMnemonic, testPassphrase string, runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
// Run init with environment variables to avoid prompts
|
||||
output, err := runSecretWithEnv(map[string]string{
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
@@ -343,12 +360,18 @@ func test01Initialize(t *testing.T, tempDir, secretPath, testMnemonic, testPassp
|
||||
|
||||
// Read and verify vault metadata content
|
||||
metadataBytes := readFile(t, vaultMetadata)
|
||||
t.Logf("Vault metadata raw content: %s", string(metadataBytes))
|
||||
|
||||
var metadata map[string]interface{}
|
||||
err = json.Unmarshal(metadataBytes, &metadata)
|
||||
require.NoError(t, err, "vault metadata should be valid JSON")
|
||||
|
||||
assert.Equal(t, "default", metadata["name"], "vault name should be default")
|
||||
t.Logf("Parsed metadata: %+v", metadata)
|
||||
|
||||
// Verify metadata fields
|
||||
assert.Equal(t, float64(0), metadata["derivation_index"], "first vault should have index 0")
|
||||
assert.Contains(t, metadata, "public_key_hash", "should contain public key hash")
|
||||
assert.Contains(t, metadata, "createdAt", "should contain creation timestamp")
|
||||
|
||||
// Verify the longterm.age file in passphrase unlocker
|
||||
longtermKeyFile := filepath.Join(passphraseUnlockerDir, "longterm.age")
|
||||
@@ -367,6 +390,10 @@ func test02ListVaults(t *testing.T, runSecret func(...string) (string, error)) {
|
||||
jsonOutput, err := runSecret("vault", "list", "--json")
|
||||
require.NoError(t, err, "vault list --json should succeed")
|
||||
|
||||
// Debug: log the raw JSON output to see what we're getting
|
||||
t.Logf("Raw JSON output: %q", jsonOutput)
|
||||
t.Logf("JSON output length: %d", len(jsonOutput))
|
||||
|
||||
// Parse JSON output
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal([]byte(jsonOutput), &response)
|
||||
@@ -481,7 +508,6 @@ func test04ImportMnemonic(t *testing.T, tempDir, testMnemonic, testPassphrase st
|
||||
err = json.Unmarshal(metadataBytes, &metadata)
|
||||
require.NoError(t, err, "vault metadata should be valid JSON")
|
||||
|
||||
assert.Equal(t, "work", metadata["name"], "vault name should be work")
|
||||
// Work vault should have a different derivation index than default (0)
|
||||
derivIndex, ok := metadata["derivation_index"].(float64)
|
||||
require.True(t, ok, "derivation_index should be a number")
|
||||
@@ -494,7 +520,7 @@ func test04ImportMnemonic(t *testing.T, tempDir, testMnemonic, testPassphrase st
|
||||
assert.NotEmpty(t, pubKeyHash, "public key hash should not be empty")
|
||||
}
|
||||
|
||||
func test05AddSecret(t *testing.T, tempDir, secretPath, testMnemonic string, runSecret func(...string) (string, error), runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
func test05AddSecret(t *testing.T, tempDir, testMnemonic string, runSecret func(...string) (string, error), runSecretWithEnv func(map[string]string, ...string) (string, error), runSecretWithStdin func(string, map[string]string, ...string) (string, error)) {
|
||||
// Switch back to default vault which has derivation index 0
|
||||
// matching our mnemonic environment variable
|
||||
_, err := runSecret("vault", "select", "default")
|
||||
@@ -502,17 +528,11 @@ func test05AddSecret(t *testing.T, tempDir, secretPath, testMnemonic string, run
|
||||
|
||||
// Add a secret with environment variables set
|
||||
secretValue := "password123"
|
||||
cmd := exec.Command(secretPath, "add", "database/password")
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("SB_SECRET_MNEMONIC=%s", testMnemonic),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
cmd.Stdin = strings.NewReader(secretValue)
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := runSecretWithStdin(secretValue, map[string]string{
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
}, "add", "database/password")
|
||||
|
||||
require.NoError(t, err, "add secret should succeed: %s", string(output))
|
||||
require.NoError(t, err, "add secret should succeed: %s", output)
|
||||
// The add command has minimal output by design
|
||||
|
||||
// Verify filesystem structure
|
||||
@@ -584,6 +604,9 @@ func test06GetSecret(t *testing.T, testMnemonic string, runSecret func(...string
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
}, "get", "database/password")
|
||||
|
||||
t.Logf("Get secret output: %q (length=%d)", output, len(output))
|
||||
t.Logf("Get secret error: %v", err)
|
||||
|
||||
require.NoError(t, err, "get secret should succeed")
|
||||
assert.Equal(t, "password123", strings.TrimSpace(output), "should return correct secret value")
|
||||
|
||||
@@ -593,24 +616,18 @@ func test06GetSecret(t *testing.T, testMnemonic string, runSecret func(...string
|
||||
assert.Contains(t, output, "failed to unlock vault", "should indicate unlock failure")
|
||||
}
|
||||
|
||||
func test07AddSecretVersion(t *testing.T, tempDir, secretPath, testMnemonic string, runSecret func(...string) (string, error), runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
func test07AddSecretVersion(t *testing.T, tempDir, testMnemonic string, runSecret func(...string) (string, error), runSecretWithEnv func(map[string]string, ...string) (string, error), runSecretWithStdin func(string, map[string]string, ...string) (string, error)) {
|
||||
// Make sure we're in default vault
|
||||
_, err := runSecret("vault", "select", "default")
|
||||
require.NoError(t, err, "vault select should succeed")
|
||||
|
||||
// Add new version of existing secret
|
||||
newSecretValue := "newpassword456"
|
||||
cmd := exec.Command(secretPath, "add", "database/password", "--force")
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("SB_SECRET_MNEMONIC=%s", testMnemonic),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
cmd.Stdin = strings.NewReader(newSecretValue)
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := runSecretWithStdin(newSecretValue, map[string]string{
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
}, "add", "database/password", "--force")
|
||||
|
||||
require.NoError(t, err, "add secret with --force should succeed: %s", string(output))
|
||||
require.NoError(t, err, "add secret with --force should succeed: %s", output)
|
||||
|
||||
// Verify filesystem structure
|
||||
defaultVaultDir := filepath.Join(tempDir, "vaults.d", "default")
|
||||
@@ -800,22 +817,16 @@ func test10PromoteVersion(t *testing.T, tempDir, testMnemonic string, runSecret
|
||||
}
|
||||
}
|
||||
|
||||
func test11ListSecrets(t *testing.T, tempDir, secretPath, testMnemonic string, runSecret func(...string) (string, error)) {
|
||||
func test11ListSecrets(t *testing.T, tempDir, testMnemonic string, runSecret func(...string) (string, error), runSecretWithStdin func(string, map[string]string, ...string) (string, error)) {
|
||||
// Make sure we're in default vault
|
||||
_, err := runSecret("vault", "select", "default")
|
||||
require.NoError(t, err, "vault select should succeed")
|
||||
|
||||
// Add a couple more secrets to make the list more interesting
|
||||
for _, secretName := range []string{"api/key", "config/database.yaml"} {
|
||||
cmd := exec.Command(secretPath, "add", secretName)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("SB_SECRET_MNEMONIC=%s", testMnemonic),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
cmd.Stdin = strings.NewReader(fmt.Sprintf("test-value-%s", secretName))
|
||||
_, err := cmd.CombinedOutput()
|
||||
_, err := runSecretWithStdin(fmt.Sprintf("test-value-%s", secretName), map[string]string{
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
}, "add", secretName)
|
||||
require.NoError(t, err, "add %s should succeed", secretName)
|
||||
}
|
||||
|
||||
@@ -878,17 +889,10 @@ func test11ListSecrets(t *testing.T, tempDir, secretPath, testMnemonic string, r
|
||||
assert.True(t, secretNames["database/password"], "should have database/password")
|
||||
}
|
||||
|
||||
func test12SecretNameFormats(t *testing.T, tempDir, secretPath, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
func test12SecretNameFormats(t *testing.T, tempDir, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error), runSecretWithStdin func(string, map[string]string, ...string) (string, error)) {
|
||||
// Make sure we're in default vault
|
||||
runSecret := func(args ...string) (string, error) {
|
||||
cmd := exec.Command(secretPath, args...)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
return string(output), err
|
||||
return cli.ExecuteCommandInProcess(args, "", nil)
|
||||
}
|
||||
|
||||
_, err := runSecret("vault", "select", "default")
|
||||
@@ -916,16 +920,10 @@ func test12SecretNameFormats(t *testing.T, tempDir, secretPath, testMnemonic str
|
||||
// Add each test secret
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.secretName, func(t *testing.T) {
|
||||
cmd := exec.Command(secretPath, "add", tc.secretName)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("SB_SECRET_MNEMONIC=%s", testMnemonic),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
cmd.Stdin = strings.NewReader(tc.value)
|
||||
output, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "add %s should succeed: %s", tc.secretName, string(output))
|
||||
output, err := runSecretWithStdin(tc.value, map[string]string{
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
}, "add", tc.secretName)
|
||||
require.NoError(t, err, "add %s should succeed: %s", tc.secretName, output)
|
||||
|
||||
// Verify filesystem storage
|
||||
secretDir := filepath.Join(secretsDir, tc.storageName)
|
||||
@@ -971,15 +969,9 @@ func test12SecretNameFormats(t *testing.T, tempDir, secretPath, testMnemonic str
|
||||
}
|
||||
|
||||
t.Run("invalid_"+testName, func(t *testing.T) {
|
||||
cmd := exec.Command(secretPath, "add", invalidName)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("SB_SECRET_MNEMONIC=%s", testMnemonic),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
cmd.Stdin = strings.NewReader("test-value")
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := runSecretWithStdin("test-value", map[string]string{
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
}, "add", invalidName)
|
||||
|
||||
// Some of these might not be invalid after all (e.g., leading/trailing slashes might be stripped, .hidden might be allowed)
|
||||
// For now, just check the ones we know should definitely fail
|
||||
@@ -1105,21 +1097,15 @@ func test14SwitchVault(t *testing.T, tempDir string, runSecret func(...string) (
|
||||
assert.Contains(t, output, "does not exist", "should indicate vault doesn't exist")
|
||||
}
|
||||
|
||||
func test15VaultIsolation(t *testing.T, tempDir, secretPath, testMnemonic string, runSecret func(...string) (string, error), runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
func test15VaultIsolation(t *testing.T, tempDir, testMnemonic string, runSecret func(...string) (string, error), runSecretWithEnv func(map[string]string, ...string) (string, error), runSecretWithStdin func(string, map[string]string, ...string) (string, error)) {
|
||||
// Make sure we're in default vault
|
||||
_, err := runSecret("vault", "select", "default")
|
||||
require.NoError(t, err, "vault select should succeed")
|
||||
|
||||
// Add a unique secret to default vault
|
||||
cmd := exec.Command(secretPath, "add", "default-only/secret", "--force")
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("SB_SECRET_MNEMONIC=%s", testMnemonic),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
cmd.Stdin = strings.NewReader("default-vault-secret")
|
||||
_, err = cmd.CombinedOutput()
|
||||
_, err = runSecretWithStdin("default-vault-secret", map[string]string{
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
}, "add", "default-only/secret", "--force")
|
||||
require.NoError(t, err, "add secret to default vault should succeed")
|
||||
|
||||
// Switch to work vault
|
||||
@@ -1134,15 +1120,9 @@ func test15VaultIsolation(t *testing.T, tempDir, secretPath, testMnemonic string
|
||||
assert.Contains(t, output, "not found", "should indicate secret not found")
|
||||
|
||||
// Add a unique secret to work vault
|
||||
cmd = exec.Command(secretPath, "add", "work-only/secret", "--force")
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("SB_SECRET_MNEMONIC=%s", testMnemonic),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
cmd.Stdin = strings.NewReader("work-vault-secret")
|
||||
_, err = cmd.CombinedOutput()
|
||||
_, err = runSecretWithStdin("work-vault-secret", map[string]string{
|
||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||
}, "add", "work-only/secret", "--force")
|
||||
require.NoError(t, err, "add secret to work vault should succeed")
|
||||
|
||||
// Switch back to default vault
|
||||
@@ -1225,17 +1205,10 @@ func test16GenerateSecret(t *testing.T, tempDir, testMnemonic string, runSecret
|
||||
verifyFileExists(t, versionsDir)
|
||||
}
|
||||
|
||||
func test17ImportFromFile(t *testing.T, tempDir, secretPath, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
func test17ImportFromFile(t *testing.T, tempDir, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error), runSecretWithStdin func(string, map[string]string, ...string) (string, error)) {
|
||||
// Make sure we're in default vault
|
||||
runSecret := func(args ...string) (string, error) {
|
||||
cmd := exec.Command(secretPath, args...)
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
||||
}
|
||||
output, err := cmd.CombinedOutput()
|
||||
return string(output), err
|
||||
return cli.ExecuteCommandInProcess(args, "", nil)
|
||||
}
|
||||
|
||||
_, err := runSecret("vault", "select", "default")
|
||||
|
||||
@@ -28,7 +28,7 @@ func newRootCmd() *cobra.Command {
|
||||
|
||||
secret.Debug("Adding subcommands to root command")
|
||||
// Add subcommands
|
||||
cmd.AddCommand(newInitCmd())
|
||||
cmd.AddCommand(NewInitCmd())
|
||||
cmd.AddCommand(newGenerateCmd())
|
||||
cmd.AddCommand(newVaultCmd())
|
||||
cmd.AddCommand(newAddCmd())
|
||||
|
||||
@@ -42,7 +42,7 @@ func newGetCmd() *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
version, _ := cmd.Flags().GetString("version")
|
||||
cli := NewCLIInstance()
|
||||
return cli.GetSecretWithVersion(args[0], version)
|
||||
return cli.GetSecretWithVersion(cmd, args[0], version)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func newListCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
cli := NewCLIInstance()
|
||||
return cli.ListSecrets(jsonOutput, filter)
|
||||
return cli.ListSecrets(cmd, jsonOutput, filter)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func newImportCmd() *cobra.Command {
|
||||
force, _ := cmd.Flags().GetBool("force")
|
||||
|
||||
cli := NewCLIInstance()
|
||||
return cli.ImportSecret(args[0], sourceFile, force)
|
||||
return cli.ImportSecret(cmd, args[0], sourceFile, force)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -135,15 +135,18 @@ func (cli *CLIInstance) AddSecret(secretName string, force bool) error {
|
||||
}
|
||||
|
||||
// GetSecret retrieves and prints a secret from the current vault
|
||||
func (cli *CLIInstance) GetSecret(secretName string) error {
|
||||
return cli.GetSecretWithVersion(secretName, "")
|
||||
func (cli *CLIInstance) GetSecret(cmd *cobra.Command, secretName string) error {
|
||||
return cli.GetSecretWithVersion(cmd, secretName, "")
|
||||
}
|
||||
|
||||
// GetSecretWithVersion retrieves and prints a specific version of a secret
|
||||
func (cli *CLIInstance) GetSecretWithVersion(secretName string, version string) error {
|
||||
func (cli *CLIInstance) GetSecretWithVersion(cmd *cobra.Command, secretName string, version string) error {
|
||||
secret.Debug("GetSecretWithVersion called", "secretName", secretName, "version", version)
|
||||
|
||||
// Get current vault
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get current vault", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -155,16 +158,20 @@ func (cli *CLIInstance) GetSecretWithVersion(secretName string, version string)
|
||||
value, err = vlt.GetSecretVersion(secretName, version)
|
||||
}
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get secret", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
secret.Debug("Got secret value", "valueLength", len(value))
|
||||
|
||||
// Print the secret value to stdout
|
||||
fmt.Print(string(value))
|
||||
cmd.Print(string(value))
|
||||
secret.Debug("Printed value to cmd")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListSecrets lists all secrets in the current vault
|
||||
func (cli *CLIInstance) ListSecrets(jsonOutput bool, filter string) error {
|
||||
func (cli *CLIInstance) ListSecrets(cmd *cobra.Command, jsonOutput bool, filter string) error {
|
||||
// Get current vault
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
@@ -220,27 +227,27 @@ func (cli *CLIInstance) ListSecrets(jsonOutput bool, filter string) error {
|
||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(jsonBytes))
|
||||
cmd.Println(string(jsonBytes))
|
||||
} else {
|
||||
// Pretty table output
|
||||
if len(filteredSecrets) == 0 {
|
||||
if filter != "" {
|
||||
fmt.Printf("No secrets found in vault '%s' matching filter '%s'.\n", vlt.GetName(), filter)
|
||||
cmd.Printf("No secrets found in vault '%s' matching filter '%s'.\n", vlt.GetName(), filter)
|
||||
} else {
|
||||
fmt.Println("No secrets found in current vault.")
|
||||
fmt.Println("Run 'secret add <name>' to create one.")
|
||||
cmd.Println("No secrets found in current vault.")
|
||||
cmd.Println("Run 'secret add <name>' to create one.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get current vault name for display
|
||||
if filter != "" {
|
||||
fmt.Printf("Secrets in vault '%s' matching '%s':\n\n", vlt.GetName(), filter)
|
||||
cmd.Printf("Secrets in vault '%s' matching '%s':\n\n", vlt.GetName(), filter)
|
||||
} else {
|
||||
fmt.Printf("Secrets in vault '%s':\n\n", vlt.GetName())
|
||||
cmd.Printf("Secrets in vault '%s':\n\n", vlt.GetName())
|
||||
}
|
||||
fmt.Printf("%-40s %-20s\n", "NAME", "LAST UPDATED")
|
||||
fmt.Printf("%-40s %-20s\n", "----", "------------")
|
||||
cmd.Printf("%-40s %-20s\n", "NAME", "LAST UPDATED")
|
||||
cmd.Printf("%-40s %-20s\n", "----", "------------")
|
||||
|
||||
for _, secretName := range filteredSecrets {
|
||||
lastUpdated := "unknown"
|
||||
@@ -248,21 +255,21 @@ func (cli *CLIInstance) ListSecrets(jsonOutput bool, filter string) error {
|
||||
metadata := secretObj.GetMetadata()
|
||||
lastUpdated = metadata.UpdatedAt.Format("2006-01-02 15:04")
|
||||
}
|
||||
fmt.Printf("%-40s %-20s\n", secretName, lastUpdated)
|
||||
cmd.Printf("%-40s %-20s\n", secretName, lastUpdated)
|
||||
}
|
||||
|
||||
fmt.Printf("\nTotal: %d secret(s)", len(filteredSecrets))
|
||||
cmd.Printf("\nTotal: %d secret(s)", len(filteredSecrets))
|
||||
if filter != "" {
|
||||
fmt.Printf(" (filtered from %d)", len(secrets))
|
||||
cmd.Printf(" (filtered from %d)", len(secrets))
|
||||
}
|
||||
fmt.Println()
|
||||
cmd.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImportSecret imports a secret from a file
|
||||
func (cli *CLIInstance) ImportSecret(secretName, sourceFile string, force bool) error {
|
||||
func (cli *CLIInstance) ImportSecret(cmd *cobra.Command, secretName, sourceFile string, force bool) error {
|
||||
// Get current vault
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
@@ -280,6 +287,6 @@ func (cli *CLIInstance) ImportSecret(secretName, sourceFile string, force bool)
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully imported secret '%s' from file '%s'\n", secretName, sourceFile)
|
||||
cmd.Printf("Successfully imported secret '%s' from file '%s'\n", secretName, sourceFile)
|
||||
return nil
|
||||
}
|
||||
|
||||
58
internal/cli/test_helpers.go
Normal file
58
internal/cli/test_helpers.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
)
|
||||
|
||||
// ExecuteCommandInProcess executes a CLI command in-process for testing
|
||||
func ExecuteCommandInProcess(args []string, stdin string, env map[string]string) (string, error) {
|
||||
secret.Debug("ExecuteCommandInProcess called", "args", args)
|
||||
|
||||
// Save current environment
|
||||
savedEnv := make(map[string]string)
|
||||
for k := range env {
|
||||
savedEnv[k] = os.Getenv(k)
|
||||
}
|
||||
|
||||
// Set test environment
|
||||
for k, v := range env {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
// Create root command
|
||||
rootCmd := newRootCmd()
|
||||
|
||||
// Capture output
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetErr(&buf)
|
||||
|
||||
// Set stdin if provided
|
||||
if stdin != "" {
|
||||
rootCmd.SetIn(strings.NewReader(stdin))
|
||||
}
|
||||
|
||||
// Set args
|
||||
rootCmd.SetArgs(args)
|
||||
|
||||
// Execute command
|
||||
err := rootCmd.Execute()
|
||||
|
||||
output := buf.String()
|
||||
secret.Debug("Command execution completed", "error", err, "outputLength", len(output), "output", output)
|
||||
|
||||
// Restore environment
|
||||
for k, v := range savedEnv {
|
||||
if v == "" {
|
||||
os.Unsetenv(k)
|
||||
} else {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return output, err
|
||||
}
|
||||
22
internal/cli/test_output_test.go
Normal file
22
internal/cli/test_output_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOutputCapture(t *testing.T) {
|
||||
// Test vault list command which we fixed
|
||||
output, err := ExecuteCommandInProcess([]string{"vault", "list"}, "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, output, "Available vaults", "should capture vault list output")
|
||||
t.Logf("vault list output: %q", output)
|
||||
|
||||
// Test help command
|
||||
output, err = ExecuteCommandInProcess([]string{"--help"}, "", nil)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, output, "help output should not be empty")
|
||||
t.Logf("help output length: %d", len(output))
|
||||
}
|
||||
@@ -38,7 +38,7 @@ func newVaultListCmd() *cobra.Command {
|
||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
cli := NewCLIInstance()
|
||||
return cli.ListVaults(jsonOutput)
|
||||
return cli.ListVaults(cmd, jsonOutput)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func newVaultCreateCmd() *cobra.Command {
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli := NewCLIInstance()
|
||||
return cli.CreateVault(args[0])
|
||||
return cli.CreateVault(cmd, args[0])
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func newVaultSelectCmd() *cobra.Command {
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cli := NewCLIInstance()
|
||||
return cli.SelectVault(args[0])
|
||||
return cli.SelectVault(cmd, args[0])
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -83,13 +83,13 @@ func newVaultImportCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
cli := NewCLIInstance()
|
||||
return cli.VaultImport(vaultName)
|
||||
return cli.VaultImport(cmd, vaultName)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ListVaults lists all available vaults
|
||||
func (cli *CLIInstance) ListVaults(jsonOutput bool) error {
|
||||
func (cli *CLIInstance) ListVaults(cmd *cobra.Command, jsonOutput bool) error {
|
||||
vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -111,12 +111,12 @@ func (cli *CLIInstance) ListVaults(jsonOutput bool) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
cmd.Println(string(jsonBytes))
|
||||
} else {
|
||||
// Text output
|
||||
fmt.Println("Available vaults:")
|
||||
cmd.Println("Available vaults:")
|
||||
if len(vaults) == 0 {
|
||||
fmt.Println(" (none)")
|
||||
cmd.Println(" (none)")
|
||||
} else {
|
||||
// Try to get current vault for marking
|
||||
currentVault := ""
|
||||
@@ -126,9 +126,9 @@ func (cli *CLIInstance) ListVaults(jsonOutput bool) error {
|
||||
|
||||
for _, vaultName := range vaults {
|
||||
if vaultName == currentVault {
|
||||
fmt.Printf(" %s (current)\n", vaultName)
|
||||
cmd.Printf(" %s (current)\n", vaultName)
|
||||
} else {
|
||||
fmt.Printf(" %s\n", vaultName)
|
||||
cmd.Printf(" %s\n", vaultName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func (cli *CLIInstance) ListVaults(jsonOutput bool) error {
|
||||
}
|
||||
|
||||
// CreateVault creates a new vault
|
||||
func (cli *CLIInstance) CreateVault(name string) error {
|
||||
func (cli *CLIInstance) CreateVault(cmd *cobra.Command, name string) error {
|
||||
secret.Debug("Creating new vault", "name", name, "state_dir", cli.stateDir)
|
||||
|
||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name)
|
||||
@@ -146,22 +146,22 @@ func (cli *CLIInstance) CreateVault(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Created vault '%s'\n", vlt.GetName())
|
||||
cmd.Printf("Created vault '%s'\n", vlt.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectVault selects a vault as the current one
|
||||
func (cli *CLIInstance) SelectVault(name string) error {
|
||||
func (cli *CLIInstance) SelectVault(cmd *cobra.Command, name string) error {
|
||||
if err := vault.SelectVault(cli.fs, cli.stateDir, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Selected vault '%s' as current\n", name)
|
||||
cmd.Printf("Selected vault '%s' as current\n", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// VaultImport imports a mnemonic into a specific vault
|
||||
func (cli *CLIInstance) VaultImport(vaultName string) error {
|
||||
func (cli *CLIInstance) VaultImport(cmd *cobra.Command, vaultName string) error {
|
||||
secret.Debug("Importing mnemonic into vault", "vault_name", vaultName, "state_dir", cli.stateDir)
|
||||
|
||||
// Get the specific vault by name
|
||||
@@ -269,9 +269,9 @@ func (cli *CLIInstance) VaultImport(vaultName string) error {
|
||||
return fmt.Errorf("failed to create unlocker: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
||||
fmt.Printf("Long-term public key: %s\n", ltPublicKey)
|
||||
fmt.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
||||
cmd.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
||||
cmd.Printf("Long-term public key: %s\n", ltPublicKey)
|
||||
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func VersionCommands(cli *CLIInstance) *cobra.Command {
|
||||
Short: "List all versions of a secret",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cli.ListVersions(args[0])
|
||||
return cli.ListVersions(cmd, args[0])
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func VersionCommands(cli *CLIInstance) *cobra.Command {
|
||||
Long: "Updates the current symlink to point to the specified version without modifying timestamps",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cli.PromoteVersion(args[0], args[1])
|
||||
return cli.PromoteVersion(cmd, args[0], args[1])
|
||||
},
|
||||
}
|
||||
|
||||
@@ -53,42 +53,46 @@ func VersionCommands(cli *CLIInstance) *cobra.Command {
|
||||
}
|
||||
|
||||
// ListVersions lists all versions of a secret
|
||||
func (cli *CLIInstance) ListVersions(secretName string) error {
|
||||
secret.Debug("Listing versions for secret", "secret_name", secretName)
|
||||
func (cli *CLIInstance) ListVersions(cmd *cobra.Command, secretName string) error {
|
||||
secret.Debug("ListVersions called", "secret_name", secretName)
|
||||
|
||||
// Get current vault
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current vault: %w", err)
|
||||
secret.Debug("Failed to get current vault", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get vault directory
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get vault directory: %w", err)
|
||||
secret.Debug("Failed to get vault directory", "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert secret name to storage name
|
||||
storageName := strings.ReplaceAll(secretName, "/", "%")
|
||||
secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
|
||||
// Get the encoded secret name
|
||||
encodedName := strings.ReplaceAll(secretName, "/", "%")
|
||||
secretDir := filepath.Join(vaultDir, "secrets.d", encodedName)
|
||||
|
||||
// Check if secret exists
|
||||
exists, err := afero.DirExists(cli.fs, secretDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to check if secret exists", "error", err)
|
||||
return fmt.Errorf("failed to check if secret exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("secret %s not found", secretName)
|
||||
secret.Debug("Secret not found", "secret_name", secretName)
|
||||
return fmt.Errorf("secret '%s' not found", secretName)
|
||||
}
|
||||
|
||||
// Get all versions
|
||||
// List all versions
|
||||
versions, err := secret.ListVersions(cli.fs, secretDir)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to list versions", "error", err)
|
||||
return fmt.Errorf("failed to list versions: %w", err)
|
||||
}
|
||||
|
||||
if len(versions) == 0 {
|
||||
fmt.Println("No versions found")
|
||||
cmd.Println("No versions found")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -155,49 +159,44 @@ func (cli *CLIInstance) ListVersions(secretName string) error {
|
||||
}
|
||||
|
||||
// PromoteVersion promotes a specific version to current
|
||||
func (cli *CLIInstance) PromoteVersion(secretName string, version string) error {
|
||||
secret.Debug("Promoting version", "secret_name", secretName, "version", version)
|
||||
|
||||
func (cli *CLIInstance) PromoteVersion(cmd *cobra.Command, secretName string, version string) error {
|
||||
// Get current vault
|
||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current vault: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get vault directory
|
||||
vaultDir, err := vlt.GetDirectory()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get vault directory: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert secret name to storage name
|
||||
storageName := strings.ReplaceAll(secretName, "/", "%")
|
||||
secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
|
||||
|
||||
// Check if secret exists
|
||||
exists, err := afero.DirExists(cli.fs, secretDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if secret exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("secret %s not found", secretName)
|
||||
}
|
||||
// Get the encoded secret name
|
||||
encodedName := strings.ReplaceAll(secretName, "/", "%")
|
||||
secretDir := filepath.Join(vaultDir, "secrets.d", encodedName)
|
||||
|
||||
// Check if version exists
|
||||
versionPath := filepath.Join(secretDir, "versions", version)
|
||||
exists, err = afero.DirExists(cli.fs, versionPath)
|
||||
versionDir := filepath.Join(secretDir, "versions", version)
|
||||
exists, err := afero.DirExists(cli.fs, versionDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if version exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return fmt.Errorf("version %s not found for secret %s", version, secretName)
|
||||
return fmt.Errorf("version '%s' not found for secret '%s'", version, secretName)
|
||||
}
|
||||
|
||||
// Update current symlink
|
||||
if err := secret.SetCurrentVersion(cli.fs, secretDir, version); err != nil {
|
||||
return fmt.Errorf("failed to promote version: %w", err)
|
||||
// Update the current symlink
|
||||
currentLink := filepath.Join(secretDir, "current")
|
||||
|
||||
// Remove existing symlink
|
||||
_ = cli.fs.Remove(currentLink)
|
||||
|
||||
// Create new symlink to the selected version
|
||||
relativePath := filepath.Join("versions", version)
|
||||
if err := afero.WriteFile(cli.fs, currentLink, []byte(relativePath), 0644); err != nil {
|
||||
return fmt.Errorf("failed to update current version: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Promoted version %s to current for secret '%s'\n", version, secretName)
|
||||
cmd.Printf("Promoted version %s to current for secret '%s'\n", version, secretName)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,8 +17,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -78,20 +77,18 @@ func TestListVersionsCommand(t *testing.T) {
|
||||
err = vlt.AddSecret("test/secret", []byte("version-2"), true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Capture output
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
// Create a command for output capture
|
||||
cmd := newRootCmd()
|
||||
var buf bytes.Buffer
|
||||
cmd.SetOut(&buf)
|
||||
cmd.SetErr(&buf)
|
||||
|
||||
// List versions
|
||||
err = cli.ListVersions("test/secret")
|
||||
err = cli.ListVersions(cmd, "test/secret")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Restore stdout and read output
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
output, _ := io.ReadAll(r)
|
||||
outputStr := string(output)
|
||||
// Read output
|
||||
outputStr := buf.String()
|
||||
|
||||
// Verify output contains version headers
|
||||
assert.Contains(t, outputStr, "VERSION")
|
||||
@@ -122,8 +119,14 @@ func TestListVersionsNonExistentSecret(t *testing.T) {
|
||||
// Set up vault with long-term key
|
||||
setupTestVault(t, fs, stateDir)
|
||||
|
||||
// Create a command for output capture
|
||||
cmd := newRootCmd()
|
||||
var buf bytes.Buffer
|
||||
cmd.SetOut(&buf)
|
||||
cmd.SetErr(&buf)
|
||||
|
||||
// Try to list versions of non-existent secret
|
||||
err := cli.ListVersions("nonexistent/secret")
|
||||
err := cli.ListVersions(cmd, "nonexistent/secret")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
@@ -163,19 +166,17 @@ func TestPromoteVersionCommand(t *testing.T) {
|
||||
// Promote first version
|
||||
firstVersion := versions[1] // Older version
|
||||
|
||||
// Capture output
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
// Create a command for output capture
|
||||
cmd := newRootCmd()
|
||||
var buf bytes.Buffer
|
||||
cmd.SetOut(&buf)
|
||||
cmd.SetErr(&buf)
|
||||
|
||||
err = cli.PromoteVersion("test/secret", firstVersion)
|
||||
err = cli.PromoteVersion(cmd, "test/secret", firstVersion)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Restore stdout and read output
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
output, _ := io.ReadAll(r)
|
||||
outputStr := string(output)
|
||||
// Read output
|
||||
outputStr := buf.String()
|
||||
|
||||
// Verify success message
|
||||
assert.Contains(t, outputStr, "Promoted version")
|
||||
@@ -202,8 +203,14 @@ func TestPromoteNonExistentVersion(t *testing.T) {
|
||||
err = vlt.AddSecret("test/secret", []byte("value"), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a command for output capture
|
||||
cmd := newRootCmd()
|
||||
var buf bytes.Buffer
|
||||
cmd.SetOut(&buf)
|
||||
cmd.SetErr(&buf)
|
||||
|
||||
// Try to promote non-existent version
|
||||
err = cli.PromoteVersion("test/secret", "20991231.999")
|
||||
err = cli.PromoteVersion(cmd, "test/secret", "20991231.999")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
@@ -235,33 +242,22 @@ func TestGetSecretWithVersion(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, versions, 2)
|
||||
|
||||
// Create a command for output capture
|
||||
cmd := newRootCmd()
|
||||
var buf bytes.Buffer
|
||||
cmd.SetOut(&buf)
|
||||
|
||||
// Test getting current version (empty version string)
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
err = cli.GetSecretWithVersion("test/secret", "")
|
||||
err = cli.GetSecretWithVersion(cmd, "test/secret", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
output, _ := io.ReadAll(r)
|
||||
|
||||
assert.Equal(t, "version-2", string(output))
|
||||
assert.Equal(t, "version-2", buf.String())
|
||||
|
||||
// Test getting specific version
|
||||
r, w, _ = os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
buf.Reset()
|
||||
firstVersion := versions[1] // Older version
|
||||
err = cli.GetSecretWithVersion("test/secret", firstVersion)
|
||||
err = cli.GetSecretWithVersion(cmd, "test/secret", firstVersion)
|
||||
require.NoError(t, err)
|
||||
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
output, _ = io.ReadAll(r)
|
||||
|
||||
assert.Equal(t, "version-1", string(output))
|
||||
assert.Equal(t, "version-1", buf.String())
|
||||
}
|
||||
|
||||
func TestVersionCommandStructure(t *testing.T) {
|
||||
@@ -296,8 +292,14 @@ func TestListVersionsEmptyOutput(t *testing.T) {
|
||||
err := fs.MkdirAll(secretDir, 0755)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a command for output capture
|
||||
cmd := newRootCmd()
|
||||
var buf bytes.Buffer
|
||||
cmd.SetOut(&buf)
|
||||
cmd.SetErr(&buf)
|
||||
|
||||
// List versions - should show "No versions found"
|
||||
err = cli.ListVersions("test/secret")
|
||||
err = cli.ListVersions(cmd, "test/secret")
|
||||
|
||||
// Should succeed even with no versions
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -136,9 +136,9 @@ func (k *KeychainUnlocker) GetID() string {
|
||||
// Generate ID using keychain item name
|
||||
keychainItemName, err := k.GetKeychainItemName()
|
||||
if err != nil {
|
||||
// Fallback to creation time-based ID if we can't read the keychain item name
|
||||
createdAt := k.Metadata.CreatedAt
|
||||
return fmt.Sprintf("%s-keychain", createdAt.Format("2006-01-02.15.04"))
|
||||
// The vault metadata is corrupt - this is a fatal error
|
||||
// We cannot continue with a fallback ID as that would mask data corruption
|
||||
panic(fmt.Sprintf("Keychain unlocker metadata is corrupt or missing keychain item name: %v", err))
|
||||
}
|
||||
return fmt.Sprintf("%s-keychain", keychainItemName)
|
||||
}
|
||||
|
||||
@@ -111,9 +111,9 @@ func (p *PGPUnlocker) GetID() string {
|
||||
// Generate ID using GPG key ID: <keyid>-pgp
|
||||
gpgKeyID, err := p.GetGPGKeyID()
|
||||
if err != nil {
|
||||
// Fallback to creation time-based ID if we can't read the GPG key ID
|
||||
createdAt := p.Metadata.CreatedAt
|
||||
return fmt.Sprintf("%s-pgp", createdAt.Format("2006-01-02.15.04"))
|
||||
// The vault metadata is corrupt - this is a fatal error
|
||||
// We cannot continue with a fallback ID as that would mask data corruption
|
||||
panic(fmt.Sprintf("PGP unlocker metadata is corrupt or missing GPG key ID: %v", err))
|
||||
}
|
||||
return fmt.Sprintf("%s-pgp", gpgKeyID)
|
||||
}
|
||||
|
||||
@@ -139,20 +139,20 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
return nil, fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
return nil, fmt.Errorf("unlocker directory %s is missing metadata file", file.Name())
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
return nil, fmt.Errorf("failed to read metadata for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
return nil, fmt.Errorf("failed to parse metadata for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
unlockers = append(unlockers, metadata)
|
||||
@@ -185,18 +185,22 @@ func (v *Vault) RemoveUnlocker(unlockerID string) error {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
if !exists {
|
||||
// Skip directories without metadata - they might not be unlockers
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
return fmt.Errorf("failed to read metadata for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
return fmt.Errorf("failed to parse metadata for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
unlockerDirPath = filepath.Join(unlockersDir, file.Name())
|
||||
@@ -255,18 +259,22 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
if !exists {
|
||||
// Skip directories without metadata - they might not be unlockers
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
return fmt.Errorf("failed to read metadata for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
return fmt.Errorf("failed to parse metadata for unlocker %s: %w", file.Name(), err)
|
||||
}
|
||||
|
||||
unlockerDirPath := filepath.Join(unlockersDir, file.Name())
|
||||
@@ -303,9 +311,11 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
|
||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||
|
||||
// Remove existing symlink if it exists
|
||||
if exists, _ := afero.Exists(v.fs, currentUnlockerPath); exists {
|
||||
if exists, err := afero.Exists(v.fs, currentUnlockerPath); err != nil {
|
||||
return fmt.Errorf("failed to check if current unlocker symlink exists: %w", err)
|
||||
} else if exists {
|
||||
if err := v.fs.Remove(currentUnlockerPath); err != nil {
|
||||
secret.Debug("Failed to remove existing unlocker symlink", "error", err, "path", currentUnlockerPath)
|
||||
return fmt.Errorf("failed to remove existing unlocker symlink: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user