package cli import ( "io" "os" "strings" "testing" "time" "path/filepath" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // 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 t.Setenv(secret.EnvMnemonic, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about") // 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()), 0600) 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) err = vlt.AddSecret("test/secret", []byte("version-1"), false) require.NoError(t, err) time.Sleep(10 * time.Millisecond) 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 // List versions err = cli.ListVersions("test/secret") require.NoError(t, err) // Restore stdout and read output w.Close() os.Stdout = oldStdout output, _ := io.ReadAll(r) outputStr := string(output) // 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) // Try to list versions of non-existent secret err := cli.ListVersions("nonexistent/secret") assert.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) err = vlt.AddSecret("test/secret", []byte("version-1"), false) require.NoError(t, err) time.Sleep(10 * time.Millisecond) err = vlt.AddSecret("test/secret", []byte("version-2"), true) require.NoError(t, err) // 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 // Capture output oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w err = cli.PromoteVersion("test/secret", firstVersion) require.NoError(t, err) // Restore stdout and read output w.Close() os.Stdout = oldStdout output, _ := io.ReadAll(r) outputStr := string(output) // 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) err = vlt.AddSecret("test/secret", []byte("value"), false) require.NoError(t, err) // Try to promote non-existent version err = cli.PromoteVersion("test/secret", "20991231.999") assert.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) err = vlt.AddSecret("test/secret", []byte("version-1"), false) require.NoError(t, err) time.Sleep(10 * time.Millisecond) err = vlt.AddSecret("test/secret", []byte("version-2"), true) require.NoError(t, err) // 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) // Test getting current version (empty version string) oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w err = cli.GetSecretWithVersion("test/secret", "") require.NoError(t, err) w.Close() os.Stdout = oldStdout output, _ := io.ReadAll(r) assert.Equal(t, "version-2", string(output)) // Test getting specific version r, w, _ = os.Pipe() os.Stdout = w firstVersion := versions[1] // Older version err = cli.GetSecretWithVersion("test/secret", firstVersion) require.NoError(t, err) w.Close() os.Stdout = oldStdout output, _ = io.ReadAll(r) assert.Equal(t, "version-1", string(output)) } func TestVersionCommandStructure(t *testing.T) { // Test that version commands are properly structured cli := NewCLIInstance() 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 ", listCmd.Use) assert.Equal(t, "List all versions of a secret", listCmd.Short) promoteCmd := cmd.Commands()[1] assert.Equal(t, "promote ", 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, 0755) require.NoError(t, err) // List versions - should show "No versions found" err = cli.ListVersions("test/secret") // Should succeed even with no versions assert.NoError(t, err) }