check / check (push) Failing after 2s
vault.CreateVault takes the unlocker passphrase and writes the vault directory, its metadata, long-term public key and passphrase unlocker into a temporary directory, renames that into vaults.d once complete, and only then makes the vault current. secret init and secret vault create call it once instead of adding the unlocker afterwards, so a kill part-way leaves either no vault, whose temporary directory the next command that takes the lock deletes, or a complete one. A test records the state directory before every change the call makes and checks each state, and the command run again from it. Model: opus-5-5
417 lines
11 KiB
Go
417 lines
11 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
|
|
// - TestGetSecretWritesBinaryValue: Tests `secret get` output of binary values
|
|
// - 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
|
|
|
|
//nolint:testpackage // white-box test of unexported internals
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
// testMnemonic is the standard BIP39 mnemonic used for CLI tests.
|
|
//nolint:dupword // BIP39 test mnemonic intentionally repeats a word
|
|
testMnemonic = "abandon abandon abandon abandon abandon abandon " +
|
|
"abandon abandon abandon abandon abandon about"
|
|
|
|
// testStateDir is the in-memory state directory used by CLI tests.
|
|
testStateDir = "/test/state"
|
|
)
|
|
|
|
// testMnemonicBuffer returns testMnemonic in a locked buffer that is
|
|
// destroyed when the test ends.
|
|
func testMnemonicBuffer(t *testing.T) *memguard.LockedBuffer {
|
|
t.Helper()
|
|
|
|
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonic))
|
|
t.Cleanup(mnemonic.Destroy)
|
|
|
|
return mnemonic
|
|
}
|
|
|
|
// Helper function to add a version of the "test/secret" secret to the
|
|
// vault with proper buffer protection
|
|
func addTestSecret(t *testing.T, vlt *vault.Vault, value []byte, force bool) {
|
|
t.Helper()
|
|
|
|
buffer := memguard.NewBufferFromBytes(value)
|
|
defer buffer.Destroy()
|
|
|
|
err := vlt.AddSecret("test/secret", buffer, force)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Helper function to set up a vault with long-term key in testStateDir
|
|
func setupTestVault(t *testing.T, fs afero.Fs) {
|
|
t.Helper()
|
|
|
|
// Create vault
|
|
vlt, err := vault.CreateVault(fs, testStateDir, "default",
|
|
testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
// Derive and store long-term key from mnemonic
|
|
ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 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, testStateDir, "default")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestListVersionsCommand(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
stateDir := testStateDir
|
|
cli := NewCLIInstanceWithStateDir(fs, stateDir)
|
|
cli.Mnemonic = testMnemonicBuffer(t)
|
|
|
|
// Set up vault with long-term key
|
|
setupTestVault(t, fs)
|
|
|
|
// Add a secret with multiple versions
|
|
vlt, err := vault.GetCurrentVault(fs, stateDir)
|
|
require.NoError(t, err)
|
|
|
|
vlt.Mnemonic = cli.Mnemonic
|
|
|
|
addTestSecret(t, vlt, []byte("version-1"), false)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
addTestSecret(t, vlt, []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) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
stateDir := testStateDir
|
|
cli := NewCLIInstanceWithStateDir(fs, stateDir)
|
|
|
|
// Set up vault with long-term key
|
|
setupTestVault(t, fs)
|
|
|
|
// 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) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
stateDir := testStateDir
|
|
cli := NewCLIInstanceWithStateDir(fs, stateDir)
|
|
|
|
// Set up vault with long-term key
|
|
setupTestVault(t, fs)
|
|
|
|
// Add a secret with multiple versions
|
|
vlt, err := vault.GetCurrentVault(fs, stateDir)
|
|
require.NoError(t, err)
|
|
|
|
vlt.Mnemonic = testMnemonicBuffer(t)
|
|
|
|
addTestSecret(t, vlt, []byte("version-1"), false)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
addTestSecret(t, vlt, []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)
|
|
|
|
defer value.Destroy()
|
|
|
|
assert.Equal(t, []byte("version-2"), value.Bytes())
|
|
|
|
// 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
|
|
promoted, err := vlt.GetSecret("test/secret")
|
|
require.NoError(t, err)
|
|
|
|
defer promoted.Destroy()
|
|
|
|
assert.Equal(t, []byte("version-1"), promoted.Bytes())
|
|
}
|
|
|
|
func TestPromoteNonExistentVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
stateDir := testStateDir
|
|
cli := NewCLIInstanceWithStateDir(fs, stateDir)
|
|
|
|
// Set up vault with long-term key
|
|
setupTestVault(t, fs)
|
|
|
|
// Add a secret
|
|
vlt, err := vault.GetCurrentVault(fs, stateDir)
|
|
require.NoError(t, err)
|
|
|
|
addTestSecret(t, vlt, []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) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
stateDir := testStateDir
|
|
cli := NewCLIInstanceWithStateDir(fs, stateDir)
|
|
cli.Mnemonic = testMnemonicBuffer(t)
|
|
|
|
// Set up vault with long-term key
|
|
setupTestVault(t, fs)
|
|
|
|
// Add a secret with multiple versions
|
|
vlt, err := vault.GetCurrentVault(fs, stateDir)
|
|
require.NoError(t, err)
|
|
|
|
vlt.Mnemonic = cli.Mnemonic
|
|
|
|
addTestSecret(t, vlt, []byte("version-1"), false)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
addTestSecret(t, vlt, []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 the current version
|
|
err = cli.GetSecret(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 TestGetSecretWritesBinaryValue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
cli := NewCLIInstanceWithStateDir(fs, testStateDir)
|
|
cli.Mnemonic = testMnemonicBuffer(t)
|
|
|
|
setupTestVault(t, fs)
|
|
|
|
vlt, err := vault.GetCurrentVault(fs, testStateDir)
|
|
require.NoError(t, err)
|
|
|
|
value := []byte{0x00, 'a', 0xff, 0xfe, 0x00, 0xc3, 0x28, 'z', 0x00}
|
|
require.False(t, utf8.Valid(value))
|
|
// A copy, since storing a value wipes the slice it came from
|
|
addTestSecret(t, vlt, bytes.Clone(value), false)
|
|
|
|
vaultDir, err := vlt.GetDirectory()
|
|
require.NoError(t, err)
|
|
|
|
versions, err := secret.ListVersions(fs,
|
|
filepath.Join(vaultDir, "secrets.d", "test%secret"))
|
|
require.NoError(t, err)
|
|
require.Len(t, versions, 1)
|
|
|
|
cmd := newRootCmd()
|
|
|
|
var buf bytes.Buffer
|
|
cmd.SetOut(&buf)
|
|
|
|
// Each writes exactly the stored bytes, with no trailing newline
|
|
err = cli.GetSecret(cmd, "test/secret")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, value, buf.Bytes())
|
|
|
|
buf.Reset()
|
|
|
|
err = cli.GetSecretWithVersion(cmd, "test/secret", versions[0])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, value, buf.Bytes())
|
|
}
|
|
|
|
//nolint:paralleltest // reads process environment to determine the state dir
|
|
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) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
stateDir := testStateDir
|
|
cli := NewCLIInstanceWithStateDir(fs, stateDir)
|
|
|
|
// Set up vault with long-term key
|
|
setupTestVault(t, fs)
|
|
|
|
// 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)
|
|
}
|