latest from ai, it broke the tests

This commit is contained in:
2025-06-20 05:40:20 -07:00
parent 6958b2a6e2
commit 0b31fba663
19 changed files with 1201 additions and 328 deletions

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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")

View File

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

View File

@@ -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
}

View 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
}

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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)