secret/internal/cli/version_test.go
clawbot 6be4601763 refactor: return errors from NewCLIInstance instead of panicking
Change NewCLIInstance() and NewCLIInstanceWithFs() to return
(*Instance, error) instead of panicking on DetermineStateDir failure.

Callers in RunE contexts propagate the error. Callers in command
construction (for shell completion) use log.Fatalf. Test callers
use t.Fatalf.

Addresses review feedback on PR #18.
2026-02-19 23:53:35 -08:00

314 lines
8.8 KiB
Go

// Version CLI Command Tests
//
// Tests for version-related CLI commands:
//
// - TestListVersionsCommand: Tests `secret version list` command output
// - TestListVersionsNonExistentSecret: Tests error handling for missing secrets
// - TestPromoteVersionCommand: Tests `secret version promote` command
// - TestPromoteNonExistentVersion: Tests error handling for invalid promotion
// - TestGetSecretWithVersion: Tests `secret get --version` flag functionality
// - TestVersionCommandStructure: Tests command structure and help text
// - TestListVersionsEmptyOutput: Tests edge case with no versions
//
// Test Utilities:
// - setupTestVault(): CLI test helper for vault initialization
// - Uses consistent test mnemonic for reproducible testing
package cli
import (
"bytes"
"path/filepath"
"strings"
"testing"
"time"
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/internal/vault"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Helper function to add a secret to vault with proper buffer protection
func addTestSecret(t *testing.T, vlt *vault.Vault, name string, value []byte, force bool) {
t.Helper()
buffer := memguard.NewBufferFromBytes(value)
defer buffer.Destroy()
err := vlt.AddSecret(name, buffer, force)
require.NoError(t, err)
}
// Helper function to set up a vault with long-term key
func setupTestVault(t *testing.T, fs afero.Fs, stateDir string) {
// Set mnemonic for testing
testMnemonic := "abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon about"
t.Setenv(secret.EnvMnemonic, testMnemonic)
// Create vault
vlt, err := vault.CreateVault(fs, stateDir, "default")
require.NoError(t, err)
// Derive and store long-term key from mnemonic
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
ltIdentity, err := agehd.DeriveIdentity(mnemonic, 0)
require.NoError(t, err)
// Store long-term public key in vault
vaultDir, _ := vlt.GetDirectory()
ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
err = afero.WriteFile(fs, ltPubKeyPath, []byte(ltIdentity.Recipient().String()), 0o600)
require.NoError(t, err)
// Select vault
err = vault.SelectVault(fs, stateDir, "default")
require.NoError(t, err)
}
func TestListVersionsCommand(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
cli := NewCLIInstanceWithStateDir(fs, stateDir)
// Set up vault with long-term key
setupTestVault(t, fs, stateDir)
// Add a secret with multiple versions
vlt, err := vault.GetCurrentVault(fs, stateDir)
require.NoError(t, err)
addTestSecret(t, vlt, "test/secret", []byte("version-1"), false)
time.Sleep(10 * time.Millisecond)
addTestSecret(t, vlt, "test/secret", []byte("version-2"), true)
// Create a command for output capture
cmd := newRootCmd()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
// List versions
err = cli.ListVersions(cmd, "test/secret")
require.NoError(t, err)
// Read output
outputStr := buf.String()
// Verify output contains version headers
assert.Contains(t, outputStr, "VERSION")
assert.Contains(t, outputStr, "CREATED")
assert.Contains(t, outputStr, "STATUS")
assert.Contains(t, outputStr, "NOT_BEFORE")
assert.Contains(t, outputStr, "NOT_AFTER")
// Should have current status for latest version
assert.Contains(t, outputStr, "current")
// Should have two version entries
lines := strings.Split(outputStr, "\n")
versionLines := 0
for _, line := range lines {
if strings.Contains(line, ".001") || strings.Contains(line, ".002") {
versionLines++
}
}
assert.Equal(t, 2, versionLines)
}
func TestListVersionsNonExistentSecret(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
cli := NewCLIInstanceWithStateDir(fs, stateDir)
// 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(cmd, "nonexistent/secret")
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestPromoteVersionCommand(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
cli := NewCLIInstanceWithStateDir(fs, stateDir)
// Set up vault with long-term key
setupTestVault(t, fs, stateDir)
// Add a secret with multiple versions
vlt, err := vault.GetCurrentVault(fs, stateDir)
require.NoError(t, err)
addTestSecret(t, vlt, "test/secret", []byte("version-1"), false)
time.Sleep(10 * time.Millisecond)
addTestSecret(t, vlt, "test/secret", []byte("version-2"), true)
// Get versions
vaultDir, _ := vlt.GetDirectory()
secretDir := vaultDir + "/secrets.d/test%secret"
versions, err := secret.ListVersions(fs, secretDir)
require.NoError(t, err)
require.Len(t, versions, 2)
// Current should be version-2
value, err := vlt.GetSecret("test/secret")
require.NoError(t, err)
assert.Equal(t, []byte("version-2"), value)
// Promote first version
firstVersion := versions[1] // Older version
// Create a command for output capture
cmd := newRootCmd()
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetErr(&buf)
err = cli.PromoteVersion(cmd, "test/secret", firstVersion)
require.NoError(t, err)
// Read output
outputStr := buf.String()
// Verify success message
assert.Contains(t, outputStr, "Promoted version")
assert.Contains(t, outputStr, firstVersion)
// Verify current is now version-1
value, err = vlt.GetSecret("test/secret")
require.NoError(t, err)
assert.Equal(t, []byte("version-1"), value)
}
func TestPromoteNonExistentVersion(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
cli := NewCLIInstanceWithStateDir(fs, stateDir)
// Set up vault with long-term key
setupTestVault(t, fs, stateDir)
// Add a secret
vlt, err := vault.GetCurrentVault(fs, stateDir)
require.NoError(t, err)
addTestSecret(t, vlt, "test/secret", []byte("value"), false)
// 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(cmd, "test/secret", "20991231.999")
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestGetSecretWithVersion(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
cli := NewCLIInstanceWithStateDir(fs, stateDir)
// Set up vault with long-term key
setupTestVault(t, fs, stateDir)
// Add a secret with multiple versions
vlt, err := vault.GetCurrentVault(fs, stateDir)
require.NoError(t, err)
addTestSecret(t, vlt, "test/secret", []byte("version-1"), false)
time.Sleep(10 * time.Millisecond)
addTestSecret(t, vlt, "test/secret", []byte("version-2"), true)
// Get versions
vaultDir, _ := vlt.GetDirectory()
secretDir := vaultDir + "/secrets.d/test%secret"
versions, err := secret.ListVersions(fs, secretDir)
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)
err = cli.GetSecretWithVersion(cmd, "test/secret", "")
require.NoError(t, err)
assert.Equal(t, "version-2", buf.String())
// Test getting specific version
buf.Reset()
firstVersion := versions[1] // Older version
err = cli.GetSecretWithVersion(cmd, "test/secret", firstVersion)
require.NoError(t, err)
assert.Equal(t, "version-1", buf.String())
}
func TestVersionCommandStructure(t *testing.T) {
// Test that version commands are properly structured
cli, err := NewCLIInstance()
if err != nil {
t.Fatalf("failed to initialize CLI: %v", err)
}
cmd := VersionCommands(cli)
assert.Equal(t, "version", cmd.Use)
assert.Equal(t, "Manage secret versions", cmd.Short)
// Check subcommands
listCmd := cmd.Commands()[0]
assert.Equal(t, "list <secret-name>", listCmd.Use)
assert.Equal(t, "List all versions of a secret", listCmd.Short)
promoteCmd := cmd.Commands()[1]
assert.Equal(t, "promote <secret-name> <version>", promoteCmd.Use)
assert.Equal(t, "Promote a specific version to current", promoteCmd.Short)
}
func TestListVersionsEmptyOutput(t *testing.T) {
fs := afero.NewMemMapFs()
stateDir := "/test/state"
cli := NewCLIInstanceWithStateDir(fs, stateDir)
// Set up vault with long-term key
setupTestVault(t, fs, stateDir)
// Create a secret directory without versions (edge case)
vaultDir := stateDir + "/vaults.d/default"
secretDir := vaultDir + "/secrets.d/test%secret"
err := fs.MkdirAll(secretDir, 0o755)
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(cmd, "test/secret")
// Should succeed even with no versions
assert.NoError(t, err)
}