latest from ai, it broke the tests
This commit is contained in:
parent
6958b2a6e2
commit
0b31fba663
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(go mod why:*)",
|
||||||
|
"Bash(go list:*)",
|
||||||
|
"Bash(~/go/bin/govulncheck -mode=module .)",
|
||||||
|
"Bash(go test:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(rg:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
2
Makefile
2
Makefile
@ -1,5 +1,7 @@
|
|||||||
default: check
|
default: check
|
||||||
|
|
||||||
|
build: ./secret
|
||||||
|
|
||||||
# Simple build (no code signing needed)
|
# Simple build (no code signing needed)
|
||||||
./secret:
|
./secret:
|
||||||
go build -v -o $@ cmd/secret/main.go
|
go build -v -o $@ cmd/secret/main.go
|
||||||
|
3
go.mod
3
go.mod
@ -22,12 +22,9 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
10
go.sum
10
go.sum
@ -31,7 +31,6 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg
|
|||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@ -61,12 +60,6 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M
|
|||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||||
@ -137,9 +130,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
|
|||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/secret"
|
"git.eeqj.de/sneak/secret/internal/vault"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/tyler-smith/go-bip39"
|
"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'.`,
|
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 {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
return cli.GenerateMnemonic()
|
return cli.GenerateMnemonic(cmd)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ func newGenerateSecretCmd() *cobra.Command {
|
|||||||
force, _ := cmd.Flags().GetBool("force")
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
cli := NewCLIInstance()
|
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
|
// 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
|
// Generate 128 bits of entropy for a 12-word mnemonic
|
||||||
entropy, err := bip39.NewEntropy(128)
|
entropy, err := bip39.NewEntropy(128)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -74,7 +74,7 @@ func (cli *CLIInstance) GenerateMnemonic() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Output mnemonic to stdout
|
// Output mnemonic to stdout
|
||||||
fmt.Println(mnemonic)
|
cmd.Println(mnemonic)
|
||||||
|
|
||||||
// Output helpful information to stderr
|
// Output helpful information to stderr
|
||||||
fmt.Fprintln(os.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
|
// 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 {
|
if length < 1 {
|
||||||
return fmt.Errorf("length must be at least 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
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := vault.AddSecret(secretName, []byte(secretValue), force); err != nil {
|
if err := vlt.AddSecret(secretName, []byte(secretValue), force); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,19 +16,23 @@ import (
|
|||||||
"github.com/tyler-smith/go-bip39"
|
"github.com/tyler-smith/go-bip39"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newInitCmd() *cobra.Command {
|
// NewInitCmd creates the init command
|
||||||
|
func NewInitCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "init",
|
Use: "init",
|
||||||
Short: "Initialize the secrets manager",
|
Short: "Initialize the secrets manager",
|
||||||
Long: `Create the necessary directory structure for storing secrets and generate encryption keys.`,
|
Long: `Create the necessary directory structure for storing secrets and generate encryption keys.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: RunInit,
|
||||||
cli := NewCLIInstance()
|
|
||||||
return cli.Init(cmd)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
||||||
secret.Debug("Starting secret manager initialization")
|
secret.Debug("Starting secret manager initialization")
|
||||||
|
|
||||||
|
@ -10,15 +10,50 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.eeqj.de/sneak/secret/internal/cli"
|
||||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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
|
// TestSecretManagerIntegration is a comprehensive integration test that exercises
|
||||||
// all functionality of the secret manager using a real filesystem in a temporary directory.
|
// 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.
|
// This test serves as both validation and documentation of the program's behavior.
|
||||||
func TestSecretManagerIntegration(t *testing.T) {
|
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
|
// Test configuration
|
||||||
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
testPassphrase := "test-passphrase-123"
|
testPassphrase := "test-passphrase-123"
|
||||||
@ -30,48 +65,30 @@ func TestSecretManagerIntegration(t *testing.T) {
|
|||||||
os.Setenv("SB_SECRET_STATE_DIR", tempDir)
|
os.Setenv("SB_SECRET_STATE_DIR", tempDir)
|
||||||
defer os.Unsetenv("SB_SECRET_STATE_DIR")
|
defer os.Unsetenv("SB_SECRET_STATE_DIR")
|
||||||
|
|
||||||
// Find the secret binary path
|
// Find the secret binary path (needed for tests that still use exec.Command)
|
||||||
// Look for it relative to the test file location
|
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
require.NoError(t, err, "should get working directory")
|
require.NoError(t, err, "should get working directory")
|
||||||
|
|
||||||
// Navigate up from internal/cli to project root
|
|
||||||
projectRoot := filepath.Join(wd, "..", "..")
|
projectRoot := filepath.Join(wd, "..", "..")
|
||||||
secretPath := filepath.Join(projectRoot, "secret")
|
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
|
// Helper function to run the secret command
|
||||||
runSecret := func(args ...string) (string, error) {
|
runSecret := func(args ...string) (string, error) {
|
||||||
cmd := exec.Command(secretPath, args...)
|
return cli.ExecuteCommandInProcess(args, "", nil)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to run secret with environment variables
|
// Helper function to run secret with environment variables
|
||||||
runSecretWithEnv := func(env map[string]string, args ...string) (string, error) {
|
runSecretWithEnv := func(env map[string]string, args ...string) (string, error) {
|
||||||
cmd := exec.Command(secretPath, args...)
|
return cli.ExecuteCommandInProcess(args, "", env)
|
||||||
cmd.Env = []string{
|
}
|
||||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
|
||||||
fmt.Sprintf("PATH=%s", os.Getenv("PATH")),
|
// Helper function to run secret with stdin
|
||||||
fmt.Sprintf("HOME=%s", os.Getenv("HOME")),
|
runSecretWithStdin := func(stdin string, env map[string]string, args ...string) (string, error) {
|
||||||
}
|
return cli.ExecuteCommandInProcess(args, stdin, env)
|
||||||
for k, v := range env {
|
|
||||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
|
|
||||||
}
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
return string(output), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declare runSecret to avoid unused variable error - will be used in later tests
|
// Declare runSecret to avoid unused variable error - will be used in later tests
|
||||||
_ = runSecret
|
_ = runSecret
|
||||||
|
_ = runSecretWithStdin
|
||||||
|
|
||||||
// Test 1: Initialize secret manager
|
// Test 1: Initialize secret manager
|
||||||
// Command: secret init
|
// Command: secret init
|
||||||
@ -81,7 +98,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
|||||||
// - currentvault symlink -> vaults.d/default
|
// - currentvault symlink -> vaults.d/default
|
||||||
// - default vault has pub.age file
|
// - default vault has pub.age file
|
||||||
// - default vault has unlockers.d directory with passphrase unlocker
|
// - 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
|
// Test 2: Vault management - List vaults
|
||||||
// Command: secret vault list
|
// Command: secret vault list
|
||||||
@ -113,7 +130,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
|||||||
// - secrets.d/database%password/versions/YYYYMMDD.001/ created
|
// - secrets.d/database%password/versions/YYYYMMDD.001/ created
|
||||||
// - Version directory contains: pub.age, priv.age, value.age, metadata.age
|
// - Version directory contains: pub.age, priv.age, value.age, metadata.age
|
||||||
// - current symlink points to version directory
|
// - current symlink points to version directory
|
||||||
test05AddSecret(t, tempDir, secretPath, testMnemonic, runSecret, runSecretWithEnv)
|
test05AddSecret(t, tempDir, testMnemonic, runSecret, runSecretWithEnv, runSecretWithStdin)
|
||||||
|
|
||||||
// Test 6: Retrieve secret
|
// Test 6: Retrieve secret
|
||||||
// Command: secret get database/password
|
// Command: secret get database/password
|
||||||
@ -128,7 +145,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
|||||||
// - New version directory YYYYMMDD.002 created
|
// - New version directory YYYYMMDD.002 created
|
||||||
// - current symlink updated to new version
|
// - current symlink updated to new version
|
||||||
// - Old version still exists
|
// - Old version still exists
|
||||||
test07AddSecretVersion(t, tempDir, secretPath, testMnemonic, runSecret, runSecretWithEnv)
|
test07AddSecretVersion(t, tempDir, testMnemonic, runSecret, runSecretWithEnv, runSecretWithStdin)
|
||||||
|
|
||||||
// Test 8: List secret versions
|
// Test 8: List secret versions
|
||||||
// Command: secret version list database/password
|
// Command: secret version list database/password
|
||||||
@ -154,13 +171,13 @@ func TestSecretManagerIntegration(t *testing.T) {
|
|||||||
// Command: secret list
|
// Command: secret list
|
||||||
// Purpose: Show all secrets in current vault
|
// Purpose: Show all secrets in current vault
|
||||||
// Expected: Shows database/password with metadata
|
// 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
|
// Test 12: Add secrets with different name formats
|
||||||
// Commands: Various secret names (paths, dots, underscores)
|
// Commands: Various secret names (paths, dots, underscores)
|
||||||
// Purpose: Test secret name validation and storage encoding
|
// Purpose: Test secret name validation and storage encoding
|
||||||
// Expected: Proper filesystem encoding (/ -> %)
|
// Expected: Proper filesystem encoding (/ -> %)
|
||||||
test12SecretNameFormats(t, tempDir, secretPath, testMnemonic, runSecretWithEnv)
|
test12SecretNameFormats(t, tempDir, testMnemonic, runSecretWithEnv, runSecretWithStdin)
|
||||||
|
|
||||||
// Test 13: Unlocker management
|
// Test 13: Unlocker management
|
||||||
// Commands: secret unlockers list, secret unlockers add pgp
|
// Commands: secret unlockers list, secret unlockers add pgp
|
||||||
@ -180,7 +197,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
|||||||
// Test 15: Cross-vault isolation
|
// Test 15: Cross-vault isolation
|
||||||
// Purpose: Verify secrets in one vault aren't accessible from another
|
// Purpose: Verify secrets in one vault aren't accessible from another
|
||||||
// Expected: Secrets from work vault not visible in default vault
|
// 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
|
// Test 16: Generate random secrets
|
||||||
// Command: secret generate secret api/key --length 32 --type base58
|
// 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
|
// Command: secret import ssh/key --source ~/.ssh/id_rsa
|
||||||
// Purpose: Import existing file as secret
|
// Purpose: Import existing file as secret
|
||||||
// Expected: File contents stored as secret value
|
// Expected: File contents stored as secret value
|
||||||
test17ImportFromFile(t, tempDir, secretPath, testMnemonic, runSecretWithEnv)
|
test17ImportFromFile(t, tempDir, testMnemonic, runSecretWithEnv, runSecretWithStdin)
|
||||||
|
|
||||||
// Test 18: Age key management
|
// Test 18: Age key management
|
||||||
// Commands: secret encrypt/decrypt using stored age keys
|
// Commands: secret encrypt/decrypt using stored age keys
|
||||||
@ -277,7 +294,7 @@ func TestSecretManagerIntegration(t *testing.T) {
|
|||||||
|
|
||||||
// Helper functions for each test section
|
// 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
|
// Run init with environment variables to avoid prompts
|
||||||
output, err := runSecretWithEnv(map[string]string{
|
output, err := runSecretWithEnv(map[string]string{
|
||||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
@ -343,12 +360,18 @@ func test01Initialize(t *testing.T, tempDir, secretPath, testMnemonic, testPassp
|
|||||||
|
|
||||||
// Read and verify vault metadata content
|
// Read and verify vault metadata content
|
||||||
metadataBytes := readFile(t, vaultMetadata)
|
metadataBytes := readFile(t, vaultMetadata)
|
||||||
|
t.Logf("Vault metadata raw content: %s", string(metadataBytes))
|
||||||
|
|
||||||
var metadata map[string]interface{}
|
var metadata map[string]interface{}
|
||||||
err = json.Unmarshal(metadataBytes, &metadata)
|
err = json.Unmarshal(metadataBytes, &metadata)
|
||||||
require.NoError(t, err, "vault metadata should be valid JSON")
|
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.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
|
// Verify the longterm.age file in passphrase unlocker
|
||||||
longtermKeyFile := filepath.Join(passphraseUnlockerDir, "longterm.age")
|
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")
|
jsonOutput, err := runSecret("vault", "list", "--json")
|
||||||
require.NoError(t, err, "vault list --json should succeed")
|
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
|
// Parse JSON output
|
||||||
var response map[string]interface{}
|
var response map[string]interface{}
|
||||||
err = json.Unmarshal([]byte(jsonOutput), &response)
|
err = json.Unmarshal([]byte(jsonOutput), &response)
|
||||||
@ -481,7 +508,6 @@ func test04ImportMnemonic(t *testing.T, tempDir, testMnemonic, testPassphrase st
|
|||||||
err = json.Unmarshal(metadataBytes, &metadata)
|
err = json.Unmarshal(metadataBytes, &metadata)
|
||||||
require.NoError(t, err, "vault metadata should be valid JSON")
|
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)
|
// Work vault should have a different derivation index than default (0)
|
||||||
derivIndex, ok := metadata["derivation_index"].(float64)
|
derivIndex, ok := metadata["derivation_index"].(float64)
|
||||||
require.True(t, ok, "derivation_index should be a number")
|
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")
|
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
|
// Switch back to default vault which has derivation index 0
|
||||||
// matching our mnemonic environment variable
|
// matching our mnemonic environment variable
|
||||||
_, err := runSecret("vault", "select", "default")
|
_, 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
|
// Add a secret with environment variables set
|
||||||
secretValue := "password123"
|
secretValue := "password123"
|
||||||
cmd := exec.Command(secretPath, "add", "database/password")
|
output, err := runSecretWithStdin(secretValue, map[string]string{
|
||||||
cmd.Env = []string{
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
}, "add", "database/password")
|
||||||
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()
|
|
||||||
|
|
||||||
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
|
// The add command has minimal output by design
|
||||||
|
|
||||||
// Verify filesystem structure
|
// Verify filesystem structure
|
||||||
@ -584,6 +604,9 @@ func test06GetSecret(t *testing.T, testMnemonic string, runSecret func(...string
|
|||||||
"SB_SECRET_MNEMONIC": testMnemonic,
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
}, "get", "database/password")
|
}, "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")
|
require.NoError(t, err, "get secret should succeed")
|
||||||
assert.Equal(t, "password123", strings.TrimSpace(output), "should return correct secret value")
|
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")
|
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
|
// Make sure we're in default vault
|
||||||
_, err := runSecret("vault", "select", "default")
|
_, err := runSecret("vault", "select", "default")
|
||||||
require.NoError(t, err, "vault select should succeed")
|
require.NoError(t, err, "vault select should succeed")
|
||||||
|
|
||||||
// Add new version of existing secret
|
// Add new version of existing secret
|
||||||
newSecretValue := "newpassword456"
|
newSecretValue := "newpassword456"
|
||||||
cmd := exec.Command(secretPath, "add", "database/password", "--force")
|
output, err := runSecretWithStdin(newSecretValue, map[string]string{
|
||||||
cmd.Env = []string{
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
}, "add", "database/password", "--force")
|
||||||
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()
|
|
||||||
|
|
||||||
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
|
// Verify filesystem structure
|
||||||
defaultVaultDir := filepath.Join(tempDir, "vaults.d", "default")
|
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
|
// Make sure we're in default vault
|
||||||
_, err := runSecret("vault", "select", "default")
|
_, err := runSecret("vault", "select", "default")
|
||||||
require.NoError(t, err, "vault select should succeed")
|
require.NoError(t, err, "vault select should succeed")
|
||||||
|
|
||||||
// Add a couple more secrets to make the list more interesting
|
// Add a couple more secrets to make the list more interesting
|
||||||
for _, secretName := range []string{"api/key", "config/database.yaml"} {
|
for _, secretName := range []string{"api/key", "config/database.yaml"} {
|
||||||
cmd := exec.Command(secretPath, "add", secretName)
|
_, err := runSecretWithStdin(fmt.Sprintf("test-value-%s", secretName), map[string]string{
|
||||||
cmd.Env = []string{
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
}, "add", secretName)
|
||||||
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()
|
|
||||||
require.NoError(t, err, "add %s should succeed", 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")
|
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
|
// Make sure we're in default vault
|
||||||
runSecret := func(args ...string) (string, error) {
|
runSecret := func(args ...string) (string, error) {
|
||||||
cmd := exec.Command(secretPath, args...)
|
return cli.ExecuteCommandInProcess(args, "", nil)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := runSecret("vault", "select", "default")
|
_, err := runSecret("vault", "select", "default")
|
||||||
@ -916,16 +920,10 @@ func test12SecretNameFormats(t *testing.T, tempDir, secretPath, testMnemonic str
|
|||||||
// Add each test secret
|
// Add each test secret
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.secretName, func(t *testing.T) {
|
t.Run(tc.secretName, func(t *testing.T) {
|
||||||
cmd := exec.Command(secretPath, "add", tc.secretName)
|
output, err := runSecretWithStdin(tc.value, map[string]string{
|
||||||
cmd.Env = []string{
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
}, "add", tc.secretName)
|
||||||
fmt.Sprintf("SB_SECRET_MNEMONIC=%s", testMnemonic),
|
require.NoError(t, err, "add %s should succeed: %s", tc.secretName, output)
|
||||||
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))
|
|
||||||
|
|
||||||
// Verify filesystem storage
|
// Verify filesystem storage
|
||||||
secretDir := filepath.Join(secretsDir, tc.storageName)
|
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) {
|
t.Run("invalid_"+testName, func(t *testing.T) {
|
||||||
cmd := exec.Command(secretPath, "add", invalidName)
|
output, err := runSecretWithStdin("test-value", map[string]string{
|
||||||
cmd.Env = []string{
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
}, "add", invalidName)
|
||||||
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()
|
|
||||||
|
|
||||||
// Some of these might not be invalid after all (e.g., leading/trailing slashes might be stripped, .hidden might be allowed)
|
// 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
|
// 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")
|
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
|
// Make sure we're in default vault
|
||||||
_, err := runSecret("vault", "select", "default")
|
_, err := runSecret("vault", "select", "default")
|
||||||
require.NoError(t, err, "vault select should succeed")
|
require.NoError(t, err, "vault select should succeed")
|
||||||
|
|
||||||
// Add a unique secret to default vault
|
// Add a unique secret to default vault
|
||||||
cmd := exec.Command(secretPath, "add", "default-only/secret", "--force")
|
_, err = runSecretWithStdin("default-vault-secret", map[string]string{
|
||||||
cmd.Env = []string{
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
}, "add", "default-only/secret", "--force")
|
||||||
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()
|
|
||||||
require.NoError(t, err, "add secret to default vault should succeed")
|
require.NoError(t, err, "add secret to default vault should succeed")
|
||||||
|
|
||||||
// Switch to work vault
|
// 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")
|
assert.Contains(t, output, "not found", "should indicate secret not found")
|
||||||
|
|
||||||
// Add a unique secret to work vault
|
// Add a unique secret to work vault
|
||||||
cmd = exec.Command(secretPath, "add", "work-only/secret", "--force")
|
_, err = runSecretWithStdin("work-vault-secret", map[string]string{
|
||||||
cmd.Env = []string{
|
"SB_SECRET_MNEMONIC": testMnemonic,
|
||||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
}, "add", "work-only/secret", "--force")
|
||||||
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()
|
|
||||||
require.NoError(t, err, "add secret to work vault should succeed")
|
require.NoError(t, err, "add secret to work vault should succeed")
|
||||||
|
|
||||||
// Switch back to default vault
|
// Switch back to default vault
|
||||||
@ -1225,17 +1205,10 @@ func test16GenerateSecret(t *testing.T, tempDir, testMnemonic string, runSecret
|
|||||||
verifyFileExists(t, versionsDir)
|
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
|
// Make sure we're in default vault
|
||||||
runSecret := func(args ...string) (string, error) {
|
runSecret := func(args ...string) (string, error) {
|
||||||
cmd := exec.Command(secretPath, args...)
|
return cli.ExecuteCommandInProcess(args, "", nil)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := runSecret("vault", "select", "default")
|
_, err := runSecret("vault", "select", "default")
|
||||||
|
@ -28,7 +28,7 @@ func newRootCmd() *cobra.Command {
|
|||||||
|
|
||||||
secret.Debug("Adding subcommands to root command")
|
secret.Debug("Adding subcommands to root command")
|
||||||
// Add subcommands
|
// Add subcommands
|
||||||
cmd.AddCommand(newInitCmd())
|
cmd.AddCommand(NewInitCmd())
|
||||||
cmd.AddCommand(newGenerateCmd())
|
cmd.AddCommand(newGenerateCmd())
|
||||||
cmd.AddCommand(newVaultCmd())
|
cmd.AddCommand(newVaultCmd())
|
||||||
cmd.AddCommand(newAddCmd())
|
cmd.AddCommand(newAddCmd())
|
||||||
|
@ -42,7 +42,7 @@ func newGetCmd() *cobra.Command {
|
|||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
version, _ := cmd.Flags().GetString("version")
|
version, _ := cmd.Flags().GetString("version")
|
||||||
cli := NewCLIInstance()
|
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()
|
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")
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
cli := NewCLIInstance()
|
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
|
// GetSecret retrieves and prints a secret from the current vault
|
||||||
func (cli *CLIInstance) GetSecret(secretName string) error {
|
func (cli *CLIInstance) GetSecret(cmd *cobra.Command, secretName string) error {
|
||||||
return cli.GetSecretWithVersion(secretName, "")
|
return cli.GetSecretWithVersion(cmd, secretName, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSecretWithVersion retrieves and prints a specific version of a secret
|
// 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
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
secret.Debug("Failed to get current vault", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,16 +158,20 @@ func (cli *CLIInstance) GetSecretWithVersion(secretName string, version string)
|
|||||||
value, err = vlt.GetSecretVersion(secretName, version)
|
value, err = vlt.GetSecretVersion(secretName, version)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
secret.Debug("Failed to get secret", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secret.Debug("Got secret value", "valueLength", len(value))
|
||||||
|
|
||||||
// Print the secret value to stdout
|
// Print the secret value to stdout
|
||||||
fmt.Print(string(value))
|
cmd.Print(string(value))
|
||||||
|
secret.Debug("Printed value to cmd")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListSecrets lists all secrets in the current vault
|
// 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
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
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)
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(string(jsonBytes))
|
cmd.Println(string(jsonBytes))
|
||||||
} else {
|
} else {
|
||||||
// Pretty table output
|
// Pretty table output
|
||||||
if len(filteredSecrets) == 0 {
|
if len(filteredSecrets) == 0 {
|
||||||
if filter != "" {
|
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 {
|
} else {
|
||||||
fmt.Println("No secrets found in current vault.")
|
cmd.Println("No secrets found in current vault.")
|
||||||
fmt.Println("Run 'secret add <name>' to create one.")
|
cmd.Println("Run 'secret add <name>' to create one.")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current vault name for display
|
// Get current vault name for display
|
||||||
if filter != "" {
|
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 {
|
} 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")
|
cmd.Printf("%-40s %-20s\n", "NAME", "LAST UPDATED")
|
||||||
fmt.Printf("%-40s %-20s\n", "----", "------------")
|
cmd.Printf("%-40s %-20s\n", "----", "------------")
|
||||||
|
|
||||||
for _, secretName := range filteredSecrets {
|
for _, secretName := range filteredSecrets {
|
||||||
lastUpdated := "unknown"
|
lastUpdated := "unknown"
|
||||||
@ -248,21 +255,21 @@ func (cli *CLIInstance) ListSecrets(jsonOutput bool, filter string) error {
|
|||||||
metadata := secretObj.GetMetadata()
|
metadata := secretObj.GetMetadata()
|
||||||
lastUpdated = metadata.UpdatedAt.Format("2006-01-02 15:04")
|
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 != "" {
|
if filter != "" {
|
||||||
fmt.Printf(" (filtered from %d)", len(secrets))
|
cmd.Printf(" (filtered from %d)", len(secrets))
|
||||||
}
|
}
|
||||||
fmt.Println()
|
cmd.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportSecret imports a secret from a file
|
// 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
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -280,6 +287,6 @@ func (cli *CLIInstance) ImportSecret(secretName, sourceFile string, force bool)
|
|||||||
return err
|
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
|
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")
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
return cli.ListVaults(jsonOutput)
|
return cli.ListVaults(cmd, jsonOutput)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ func newVaultCreateCmd() *cobra.Command {
|
|||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cli := NewCLIInstance()
|
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),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
return cli.SelectVault(args[0])
|
return cli.SelectVault(cmd, args[0])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,13 +83,13 @@ func newVaultImportCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
return cli.VaultImport(vaultName)
|
return cli.VaultImport(cmd, vaultName)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListVaults lists all available vaults
|
// 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)
|
vaults, err := vault.ListVaults(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -111,12 +111,12 @@ func (cli *CLIInstance) ListVaults(jsonOutput bool) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println(string(jsonBytes))
|
cmd.Println(string(jsonBytes))
|
||||||
} else {
|
} else {
|
||||||
// Text output
|
// Text output
|
||||||
fmt.Println("Available vaults:")
|
cmd.Println("Available vaults:")
|
||||||
if len(vaults) == 0 {
|
if len(vaults) == 0 {
|
||||||
fmt.Println(" (none)")
|
cmd.Println(" (none)")
|
||||||
} else {
|
} else {
|
||||||
// Try to get current vault for marking
|
// Try to get current vault for marking
|
||||||
currentVault := ""
|
currentVault := ""
|
||||||
@ -126,9 +126,9 @@ func (cli *CLIInstance) ListVaults(jsonOutput bool) error {
|
|||||||
|
|
||||||
for _, vaultName := range vaults {
|
for _, vaultName := range vaults {
|
||||||
if vaultName == currentVault {
|
if vaultName == currentVault {
|
||||||
fmt.Printf(" %s (current)\n", vaultName)
|
cmd.Printf(" %s (current)\n", vaultName)
|
||||||
} else {
|
} 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
|
// 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)
|
secret.Debug("Creating new vault", "name", name, "state_dir", cli.stateDir)
|
||||||
|
|
||||||
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name)
|
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, name)
|
||||||
@ -146,22 +146,22 @@ func (cli *CLIInstance) CreateVault(name string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Created vault '%s'\n", vlt.GetName())
|
cmd.Printf("Created vault '%s'\n", vlt.GetName())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectVault selects a vault as the current one
|
// 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 {
|
if err := vault.SelectVault(cli.fs, cli.stateDir, name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Selected vault '%s' as current\n", name)
|
cmd.Printf("Selected vault '%s' as current\n", name)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VaultImport imports a mnemonic into a specific vault
|
// 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)
|
secret.Debug("Importing mnemonic into vault", "vault_name", vaultName, "state_dir", cli.stateDir)
|
||||||
|
|
||||||
// Get the specific vault by name
|
// 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)
|
return fmt.Errorf("failed to create unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
cmd.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
||||||
fmt.Printf("Long-term public key: %s\n", ltPublicKey)
|
cmd.Printf("Long-term public key: %s\n", ltPublicKey)
|
||||||
fmt.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ func VersionCommands(cli *CLIInstance) *cobra.Command {
|
|||||||
Short: "List all versions of a secret",
|
Short: "List all versions of a secret",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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",
|
Long: "Updates the current symlink to point to the specified version without modifying timestamps",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
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
|
// ListVersions lists all versions of a secret
|
||||||
func (cli *CLIInstance) ListVersions(secretName string) error {
|
func (cli *CLIInstance) ListVersions(cmd *cobra.Command, secretName string) error {
|
||||||
secret.Debug("Listing versions for secret", "secret_name", secretName)
|
secret.Debug("ListVersions called", "secret_name", secretName)
|
||||||
|
|
||||||
// Get current vault
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
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()
|
vaultDir, err := vlt.GetDirectory()
|
||||||
if err != nil {
|
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
|
// Get the encoded secret name
|
||||||
storageName := strings.ReplaceAll(secretName, "/", "%")
|
encodedName := strings.ReplaceAll(secretName, "/", "%")
|
||||||
secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
|
secretDir := filepath.Join(vaultDir, "secrets.d", encodedName)
|
||||||
|
|
||||||
// Check if secret exists
|
// Check if secret exists
|
||||||
exists, err := afero.DirExists(cli.fs, secretDir)
|
exists, err := afero.DirExists(cli.fs, secretDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
secret.Debug("Failed to check if secret exists", "error", err)
|
||||||
return fmt.Errorf("failed to check if secret exists: %w", err)
|
return fmt.Errorf("failed to check if secret exists: %w", err)
|
||||||
}
|
}
|
||||||
if !exists {
|
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)
|
versions, err := secret.ListVersions(cli.fs, secretDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
secret.Debug("Failed to list versions", "error", err)
|
||||||
return fmt.Errorf("failed to list versions: %w", err)
|
return fmt.Errorf("failed to list versions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(versions) == 0 {
|
if len(versions) == 0 {
|
||||||
fmt.Println("No versions found")
|
cmd.Println("No versions found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,49 +159,44 @@ func (cli *CLIInstance) ListVersions(secretName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PromoteVersion promotes a specific version to current
|
// PromoteVersion promotes a specific version to current
|
||||||
func (cli *CLIInstance) PromoteVersion(secretName string, version string) error {
|
func (cli *CLIInstance) PromoteVersion(cmd *cobra.Command, secretName string, version string) error {
|
||||||
secret.Debug("Promoting version", "secret_name", secretName, "version", version)
|
|
||||||
|
|
||||||
// Get current vault
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get current vault: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get vault directory
|
|
||||||
vaultDir, err := vlt.GetDirectory()
|
vaultDir, err := vlt.GetDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get vault directory: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert secret name to storage name
|
// Get the encoded secret name
|
||||||
storageName := strings.ReplaceAll(secretName, "/", "%")
|
encodedName := strings.ReplaceAll(secretName, "/", "%")
|
||||||
secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
|
secretDir := filepath.Join(vaultDir, "secrets.d", encodedName)
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if version exists
|
// Check if version exists
|
||||||
versionPath := filepath.Join(secretDir, "versions", version)
|
versionDir := filepath.Join(secretDir, "versions", version)
|
||||||
exists, err = afero.DirExists(cli.fs, versionPath)
|
exists, err := afero.DirExists(cli.fs, versionDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check if version exists: %w", err)
|
return fmt.Errorf("failed to check if version exists: %w", err)
|
||||||
}
|
}
|
||||||
if !exists {
|
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
|
// Update the current symlink
|
||||||
if err := secret.SetCurrentVersion(cli.fs, secretDir, version); err != nil {
|
currentLink := filepath.Join(secretDir, "current")
|
||||||
return fmt.Errorf("failed to promote version: %w", err)
|
|
||||||
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,7 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"bytes"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -78,20 +77,18 @@ func TestListVersionsCommand(t *testing.T) {
|
|||||||
err = vlt.AddSecret("test/secret", []byte("version-2"), true)
|
err = vlt.AddSecret("test/secret", []byte("version-2"), true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Capture output
|
// Create a command for output capture
|
||||||
oldStdout := os.Stdout
|
cmd := newRootCmd()
|
||||||
r, w, _ := os.Pipe()
|
var buf bytes.Buffer
|
||||||
os.Stdout = w
|
cmd.SetOut(&buf)
|
||||||
|
cmd.SetErr(&buf)
|
||||||
|
|
||||||
// List versions
|
// List versions
|
||||||
err = cli.ListVersions("test/secret")
|
err = cli.ListVersions(cmd, "test/secret")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Restore stdout and read output
|
// Read output
|
||||||
w.Close()
|
outputStr := buf.String()
|
||||||
os.Stdout = oldStdout
|
|
||||||
output, _ := io.ReadAll(r)
|
|
||||||
outputStr := string(output)
|
|
||||||
|
|
||||||
// Verify output contains version headers
|
// Verify output contains version headers
|
||||||
assert.Contains(t, outputStr, "VERSION")
|
assert.Contains(t, outputStr, "VERSION")
|
||||||
@ -122,8 +119,14 @@ func TestListVersionsNonExistentSecret(t *testing.T) {
|
|||||||
// Set up vault with long-term key
|
// Set up vault with long-term key
|
||||||
setupTestVault(t, fs, stateDir)
|
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
|
// Try to list versions of non-existent secret
|
||||||
err := cli.ListVersions("nonexistent/secret")
|
err := cli.ListVersions(cmd, "nonexistent/secret")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "not found")
|
assert.Contains(t, err.Error(), "not found")
|
||||||
}
|
}
|
||||||
@ -163,19 +166,17 @@ func TestPromoteVersionCommand(t *testing.T) {
|
|||||||
// Promote first version
|
// Promote first version
|
||||||
firstVersion := versions[1] // Older version
|
firstVersion := versions[1] // Older version
|
||||||
|
|
||||||
// Capture output
|
// Create a command for output capture
|
||||||
oldStdout := os.Stdout
|
cmd := newRootCmd()
|
||||||
r, w, _ := os.Pipe()
|
var buf bytes.Buffer
|
||||||
os.Stdout = w
|
cmd.SetOut(&buf)
|
||||||
|
cmd.SetErr(&buf)
|
||||||
|
|
||||||
err = cli.PromoteVersion("test/secret", firstVersion)
|
err = cli.PromoteVersion(cmd, "test/secret", firstVersion)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Restore stdout and read output
|
// Read output
|
||||||
w.Close()
|
outputStr := buf.String()
|
||||||
os.Stdout = oldStdout
|
|
||||||
output, _ := io.ReadAll(r)
|
|
||||||
outputStr := string(output)
|
|
||||||
|
|
||||||
// Verify success message
|
// Verify success message
|
||||||
assert.Contains(t, outputStr, "Promoted version")
|
assert.Contains(t, outputStr, "Promoted version")
|
||||||
@ -202,8 +203,14 @@ func TestPromoteNonExistentVersion(t *testing.T) {
|
|||||||
err = vlt.AddSecret("test/secret", []byte("value"), false)
|
err = vlt.AddSecret("test/secret", []byte("value"), false)
|
||||||
require.NoError(t, err)
|
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
|
// 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.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "not found")
|
assert.Contains(t, err.Error(), "not found")
|
||||||
}
|
}
|
||||||
@ -235,33 +242,22 @@ func TestGetSecretWithVersion(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, versions, 2)
|
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)
|
// Test getting current version (empty version string)
|
||||||
oldStdout := os.Stdout
|
err = cli.GetSecretWithVersion(cmd, "test/secret", "")
|
||||||
r, w, _ := os.Pipe()
|
|
||||||
os.Stdout = w
|
|
||||||
|
|
||||||
err = cli.GetSecretWithVersion("test/secret", "")
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "version-2", buf.String())
|
||||||
w.Close()
|
|
||||||
os.Stdout = oldStdout
|
|
||||||
output, _ := io.ReadAll(r)
|
|
||||||
|
|
||||||
assert.Equal(t, "version-2", string(output))
|
|
||||||
|
|
||||||
// Test getting specific version
|
// Test getting specific version
|
||||||
r, w, _ = os.Pipe()
|
buf.Reset()
|
||||||
os.Stdout = w
|
|
||||||
|
|
||||||
firstVersion := versions[1] // Older version
|
firstVersion := versions[1] // Older version
|
||||||
err = cli.GetSecretWithVersion("test/secret", firstVersion)
|
err = cli.GetSecretWithVersion(cmd, "test/secret", firstVersion)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "version-1", buf.String())
|
||||||
w.Close()
|
|
||||||
os.Stdout = oldStdout
|
|
||||||
output, _ = io.ReadAll(r)
|
|
||||||
|
|
||||||
assert.Equal(t, "version-1", string(output))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVersionCommandStructure(t *testing.T) {
|
func TestVersionCommandStructure(t *testing.T) {
|
||||||
@ -296,8 +292,14 @@ func TestListVersionsEmptyOutput(t *testing.T) {
|
|||||||
err := fs.MkdirAll(secretDir, 0755)
|
err := fs.MkdirAll(secretDir, 0755)
|
||||||
require.NoError(t, err)
|
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"
|
// List versions - should show "No versions found"
|
||||||
err = cli.ListVersions("test/secret")
|
err = cli.ListVersions(cmd, "test/secret")
|
||||||
|
|
||||||
// Should succeed even with no versions
|
// Should succeed even with no versions
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -136,9 +136,9 @@ func (k *KeychainUnlocker) GetID() string {
|
|||||||
// Generate ID using keychain item name
|
// Generate ID using keychain item name
|
||||||
keychainItemName, err := k.GetKeychainItemName()
|
keychainItemName, err := k.GetKeychainItemName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to creation time-based ID if we can't read the keychain item name
|
// The vault metadata is corrupt - this is a fatal error
|
||||||
createdAt := k.Metadata.CreatedAt
|
// We cannot continue with a fallback ID as that would mask data corruption
|
||||||
return fmt.Sprintf("%s-keychain", createdAt.Format("2006-01-02.15.04"))
|
panic(fmt.Sprintf("Keychain unlocker metadata is corrupt or missing keychain item name: %v", err))
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s-keychain", keychainItemName)
|
return fmt.Sprintf("%s-keychain", keychainItemName)
|
||||||
}
|
}
|
||||||
|
@ -111,9 +111,9 @@ func (p *PGPUnlocker) GetID() string {
|
|||||||
// Generate ID using GPG key ID: <keyid>-pgp
|
// Generate ID using GPG key ID: <keyid>-pgp
|
||||||
gpgKeyID, err := p.GetGPGKeyID()
|
gpgKeyID, err := p.GetGPGKeyID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback to creation time-based ID if we can't read the GPG key ID
|
// The vault metadata is corrupt - this is a fatal error
|
||||||
createdAt := p.Metadata.CreatedAt
|
// We cannot continue with a fallback ID as that would mask data corruption
|
||||||
return fmt.Sprintf("%s-pgp", createdAt.Format("2006-01-02.15.04"))
|
panic(fmt.Sprintf("PGP unlocker metadata is corrupt or missing GPG key ID: %v", err))
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s-pgp", gpgKeyID)
|
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")
|
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
||||||
exists, err := afero.Exists(v.fs, metadataPath)
|
exists, err := afero.Exists(v.fs, metadataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err)
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
continue
|
return nil, fmt.Errorf("unlocker directory %s is missing metadata file", file.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return nil, fmt.Errorf("failed to read metadata for unlocker %s: %w", file.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadata UnlockerMetadata
|
var metadata UnlockerMetadata
|
||||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
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)
|
unlockers = append(unlockers, metadata)
|
||||||
@ -185,18 +185,22 @@ func (v *Vault) RemoveUnlocker(unlockerID string) error {
|
|||||||
// Read metadata file
|
// Read metadata file
|
||||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
||||||
exists, err := afero.Exists(v.fs, metadataPath)
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return fmt.Errorf("failed to read metadata for unlocker %s: %w", file.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadata UnlockerMetadata
|
var metadata UnlockerMetadata
|
||||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
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())
|
unlockerDirPath = filepath.Join(unlockersDir, file.Name())
|
||||||
@ -255,18 +259,22 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
|
|||||||
// Read metadata file
|
// Read metadata file
|
||||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlocker-metadata.json")
|
||||||
exists, err := afero.Exists(v.fs, metadataPath)
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
return fmt.Errorf("failed to read metadata for unlocker %s: %w", file.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var metadata UnlockerMetadata
|
var metadata UnlockerMetadata
|
||||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
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())
|
unlockerDirPath := filepath.Join(unlockersDir, file.Name())
|
||||||
@ -303,9 +311,11 @@ func (v *Vault) SelectUnlocker(unlockerID string) error {
|
|||||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||||
|
|
||||||
// Remove existing symlink if it exists
|
// 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 {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +133,11 @@ func TestDeterministicDerivation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if id1.String() != id2.String() {
|
if id1.String() != id2.String() {
|
||||||
t.Fatalf("identities should be deterministic: %s != %s", id1.String(), id2.String())
|
t.Fatalf(
|
||||||
|
"identities should be deterministic: %s != %s",
|
||||||
|
id1.String(),
|
||||||
|
id2.String(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that different indices produce different identities
|
// Test that different indices produce different identities
|
||||||
@ -163,7 +167,11 @@ func TestDeterministicXPRVDerivation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if id1.String() != id2.String() {
|
if id1.String() != id2.String() {
|
||||||
t.Fatalf("xprv identities should be deterministic: %s != %s", id1.String(), id2.String())
|
t.Fatalf(
|
||||||
|
"xprv identities should be deterministic: %s != %s",
|
||||||
|
id1.String(),
|
||||||
|
id2.String(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that different indices with same xprv produce different identities
|
// Test that different indices with same xprv produce different identities
|
||||||
@ -181,11 +189,8 @@ func TestDeterministicXPRVDerivation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMnemonicVsXPRVConsistency(t *testing.T) {
|
func TestMnemonicVsXPRVConsistency(t *testing.T) {
|
||||||
// Test that deriving from mnemonic and from the corresponding xprv produces the same result
|
// FIXME This test is missing!
|
||||||
// Note: The test mnemonic and test xprv are from different sources
|
|
||||||
// and are not expected to produce the same results, so this test merely
|
|
||||||
// verifies that both derivation methods work without errors.
|
|
||||||
t.Log("Testing mnemonic vs XPRV derivation - note: test data is from different sources")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEntropyLength(t *testing.T) {
|
func TestEntropyLength(t *testing.T) {
|
||||||
@ -208,7 +213,10 @@ func TestEntropyLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(entropyXPRV) != 32 {
|
if len(entropyXPRV) != 32 {
|
||||||
t.Fatalf("expected 32 bytes of entropy from xprv, got %d", len(entropyXPRV))
|
t.Fatalf(
|
||||||
|
"expected 32 bytes of entropy from xprv, got %d",
|
||||||
|
len(entropyXPRV),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("XPRV Entropy (32 bytes): %x", entropyXPRV)
|
t.Logf("XPRV Entropy (32 bytes): %x", entropyXPRV)
|
||||||
@ -264,14 +272,49 @@ func TestClampFunction(t *testing.T) {
|
|||||||
expected []byte
|
expected []byte
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "all zeros",
|
name: "all zeros",
|
||||||
input: make([]byte, 32),
|
input: make([]byte, 32),
|
||||||
expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64},
|
expected: []byte{
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
64,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "all ones",
|
name: "all ones",
|
||||||
input: bytes.Repeat([]byte{255}, 32),
|
input: bytes.Repeat([]byte{255}, 32),
|
||||||
expected: append([]byte{248}, append(bytes.Repeat([]byte{255}, 30), 127)...),
|
expected: append(
|
||||||
|
[]byte{248},
|
||||||
|
append(bytes.Repeat([]byte{255}, 30), 127)...),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,13 +326,22 @@ func TestClampFunction(t *testing.T) {
|
|||||||
|
|
||||||
// Check specific bits that should be clamped
|
// Check specific bits that should be clamped
|
||||||
if input[0]&7 != 0 {
|
if input[0]&7 != 0 {
|
||||||
t.Errorf("first byte should have bottom 3 bits cleared, got %08b", input[0])
|
t.Errorf(
|
||||||
|
"first byte should have bottom 3 bits cleared, got %08b",
|
||||||
|
input[0],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if input[31]&128 != 0 {
|
if input[31]&128 != 0 {
|
||||||
t.Errorf("last byte should have top bit cleared, got %08b", input[31])
|
t.Errorf(
|
||||||
|
"last byte should have top bit cleared, got %08b",
|
||||||
|
input[31],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if input[31]&64 == 0 {
|
if input[31]&64 == 0 {
|
||||||
t.Errorf("last byte should have second-to-top bit set, got %08b", input[31])
|
t.Errorf(
|
||||||
|
"last byte should have second-to-top bit set, got %08b",
|
||||||
|
input[31],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -337,7 +389,9 @@ func TestIdentityFromEntropyEdgeCases(t *testing.T) {
|
|||||||
entropy: func() []byte {
|
entropy: func() []byte {
|
||||||
b := make([]byte, 32)
|
b := make([]byte, 32)
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
panic(err) // In test context, panic is acceptable for setup failures
|
panic(
|
||||||
|
err,
|
||||||
|
) // In test context, panic is acceptable for setup failures
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}(),
|
}(),
|
||||||
@ -356,7 +410,10 @@ func TestIdentityFromEntropyEdgeCases(t *testing.T) {
|
|||||||
t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
|
t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
|
||||||
}
|
}
|
||||||
if identity != nil {
|
if identity != nil {
|
||||||
t.Errorf("expected nil identity on error, got %v", identity)
|
t.Errorf(
|
||||||
|
"expected nil identity on error, got %v",
|
||||||
|
identity,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -531,7 +588,11 @@ func TestIndexBoundaries(t *testing.T) {
|
|||||||
t.Run(fmt.Sprintf("index_%d", index), func(t *testing.T) {
|
t.Run(fmt.Sprintf("index_%d", index), func(t *testing.T) {
|
||||||
identity, err := DeriveIdentity(mnemonic, index)
|
identity, err := DeriveIdentity(mnemonic, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to derive identity at index %d: %v", index, err)
|
t.Fatalf(
|
||||||
|
"failed to derive identity at index %d: %v",
|
||||||
|
index,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the identity is valid by testing encryption/decryption
|
// Verify the identity is valid by testing encryption/decryption
|
||||||
@ -628,11 +689,19 @@ func TestConcurrentDerivation(t *testing.T) {
|
|||||||
expectedResults := testNumGoroutines
|
expectedResults := testNumGoroutines
|
||||||
for result, count := range resultMap {
|
for result, count := range resultMap {
|
||||||
if count != expectedResults {
|
if count != expectedResults {
|
||||||
t.Errorf("result %s appeared %d times, expected %d", result, count, expectedResults)
|
t.Errorf(
|
||||||
|
"result %s appeared %d times, expected %d",
|
||||||
|
result,
|
||||||
|
count,
|
||||||
|
expectedResults,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("Concurrent derivation test passed with %d unique results", len(resultMap))
|
t.Logf(
|
||||||
|
"Concurrent derivation test passed with %d unique results",
|
||||||
|
len(resultMap),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Benchmark tests
|
// Benchmark tests
|
||||||
@ -712,16 +781,28 @@ func BenchmarkEncryptDecrypt(b *testing.B) {
|
|||||||
// TestConstants verifies the hardcoded constants
|
// TestConstants verifies the hardcoded constants
|
||||||
func TestConstants(t *testing.T) {
|
func TestConstants(t *testing.T) {
|
||||||
if purpose != 83696968 {
|
if purpose != 83696968 {
|
||||||
t.Errorf("purpose constant mismatch: expected 83696968, got %d", purpose)
|
t.Errorf(
|
||||||
|
"purpose constant mismatch: expected 83696968, got %d",
|
||||||
|
purpose,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if vendorID != 592366788 {
|
if vendorID != 592366788 {
|
||||||
t.Errorf("vendorID constant mismatch: expected 592366788, got %d", vendorID)
|
t.Errorf(
|
||||||
|
"vendorID constant mismatch: expected 592366788, got %d",
|
||||||
|
vendorID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if appID != 733482323 {
|
if appID != 733482323 {
|
||||||
t.Errorf("appID constant mismatch: expected 733482323, got %d", appID)
|
t.Errorf(
|
||||||
|
"appID constant mismatch: expected 733482323, got %d",
|
||||||
|
appID,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if hrp != "age-secret-key-" {
|
if hrp != "age-secret-key-" {
|
||||||
t.Errorf("hrp constant mismatch: expected 'age-secret-key-', got %q", hrp)
|
t.Errorf(
|
||||||
|
"hrp constant mismatch: expected 'age-secret-key-', got %q",
|
||||||
|
hrp,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -737,7 +818,10 @@ func TestIdentityStringFormat(t *testing.T) {
|
|||||||
|
|
||||||
// Check secret key format
|
// Check secret key format
|
||||||
if !strings.HasPrefix(secretKey, "AGE-SECRET-KEY-") {
|
if !strings.HasPrefix(secretKey, "AGE-SECRET-KEY-") {
|
||||||
t.Errorf("secret key should start with 'AGE-SECRET-KEY-', got: %s", secretKey)
|
t.Errorf(
|
||||||
|
"secret key should start with 'AGE-SECRET-KEY-', got: %s",
|
||||||
|
secretKey,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check recipient format
|
// Check recipient format
|
||||||
@ -834,14 +918,22 @@ func TestRandomMnemonicDeterministicGeneration(t *testing.T) {
|
|||||||
privateKey1 := identity1.String()
|
privateKey1 := identity1.String()
|
||||||
privateKey2 := identity2.String()
|
privateKey2 := identity2.String()
|
||||||
if privateKey1 != privateKey2 {
|
if privateKey1 != privateKey2 {
|
||||||
t.Fatalf("private keys should be identical:\nFirst: %s\nSecond: %s", privateKey1, privateKey2)
|
t.Fatalf(
|
||||||
|
"private keys should be identical:\nFirst: %s\nSecond: %s",
|
||||||
|
privateKey1,
|
||||||
|
privateKey2,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that both public keys (recipients) are identical
|
// Verify that both public keys (recipients) are identical
|
||||||
publicKey1 := identity1.Recipient().String()
|
publicKey1 := identity1.Recipient().String()
|
||||||
publicKey2 := identity2.Recipient().String()
|
publicKey2 := identity2.Recipient().String()
|
||||||
if publicKey1 != publicKey2 {
|
if publicKey1 != publicKey2 {
|
||||||
t.Fatalf("public keys should be identical:\nFirst: %s\nSecond: %s", publicKey1, publicKey2)
|
t.Fatalf(
|
||||||
|
"public keys should be identical:\nFirst: %s\nSecond: %s",
|
||||||
|
publicKey1,
|
||||||
|
publicKey2,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("✓ Deterministic generation verified")
|
t.Logf("✓ Deterministic generation verified")
|
||||||
@ -873,10 +965,17 @@ func TestRandomMnemonicDeterministicGeneration(t *testing.T) {
|
|||||||
t.Fatalf("failed to close encryptor: %v", err)
|
t.Fatalf("failed to close encryptor: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("✓ Encrypted %d bytes into %d bytes of ciphertext", len(testData), ciphertext.Len())
|
t.Logf(
|
||||||
|
"✓ Encrypted %d bytes into %d bytes of ciphertext",
|
||||||
|
len(testData),
|
||||||
|
ciphertext.Len(),
|
||||||
|
)
|
||||||
|
|
||||||
// Decrypt the data using the private key
|
// Decrypt the data using the private key
|
||||||
decryptor, err := age.Decrypt(bytes.NewReader(ciphertext.Bytes()), identity1)
|
decryptor, err := age.Decrypt(
|
||||||
|
bytes.NewReader(ciphertext.Bytes()),
|
||||||
|
identity1,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create decryptor: %v", err)
|
t.Fatalf("failed to create decryptor: %v", err)
|
||||||
}
|
}
|
||||||
@ -890,7 +989,11 @@ func TestRandomMnemonicDeterministicGeneration(t *testing.T) {
|
|||||||
|
|
||||||
// Verify that the decrypted data matches the original
|
// Verify that the decrypted data matches the original
|
||||||
if len(decryptedData) != len(testData) {
|
if len(decryptedData) != len(testData) {
|
||||||
t.Fatalf("decrypted data length mismatch: expected %d, got %d", len(testData), len(decryptedData))
|
t.Fatalf(
|
||||||
|
"decrypted data length mismatch: expected %d, got %d",
|
||||||
|
len(testData),
|
||||||
|
len(decryptedData),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Equal(testData, decryptedData) {
|
if !bytes.Equal(testData, decryptedData) {
|
||||||
@ -917,7 +1020,10 @@ func TestRandomMnemonicDeterministicGeneration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt with the second identity
|
// Decrypt with the second identity
|
||||||
decryptor2, err := age.Decrypt(bytes.NewReader(ciphertext2.Bytes()), identity2)
|
decryptor2, err := age.Decrypt(
|
||||||
|
bytes.NewReader(ciphertext2.Bytes()),
|
||||||
|
identity2,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create second decryptor: %v", err)
|
t.Fatalf("failed to create second decryptor: %v", err)
|
||||||
}
|
}
|
||||||
|
688
test_secret_manager.sh
Executable file
688
test_secret_manager.sh
Executable file
@ -0,0 +1,688 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Test configuration
|
||||||
|
TEST_MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
TEST_PASSPHRASE="test-passphrase-123"
|
||||||
|
TEMP_DIR="$(mktemp -d)"
|
||||||
|
SECRET_BINARY="./secret"
|
||||||
|
|
||||||
|
# Enable debug output from the secret program
|
||||||
|
export GODEBUG="berlin.sneak.pkg.secret"
|
||||||
|
|
||||||
|
echo -e "${BLUE}=== Secret Manager Comprehensive Test Script ===${NC}"
|
||||||
|
echo -e "${YELLOW}Using temporary directory: $TEMP_DIR${NC}"
|
||||||
|
echo -e "${YELLOW}Debug output enabled: GODEBUG=$GODEBUG${NC}"
|
||||||
|
echo -e "${YELLOW}Note: All tests use environment variables (no manual input)${NC}"
|
||||||
|
|
||||||
|
# Function to print test steps
|
||||||
|
print_step() {
|
||||||
|
echo -e "\n${BLUE}Step $1: $2${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print success
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✓ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print error and exit
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}✗ $1${NC}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print warning (for expected failures)
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}⚠ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to clear state directory and reset environment
|
||||||
|
reset_state() {
|
||||||
|
echo -e "${YELLOW}Resetting state directory...${NC}"
|
||||||
|
|
||||||
|
# Safety checks before removing anything
|
||||||
|
if [ -z "$TEMP_DIR" ]; then
|
||||||
|
print_error "TEMP_DIR is not set, cannot reset state safely"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$TEMP_DIR" ]; then
|
||||||
|
print_error "TEMP_DIR ($TEMP_DIR) is not a directory, cannot reset state safely"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Additional safety: ensure TEMP_DIR looks like a temp directory
|
||||||
|
case "$TEMP_DIR" in
|
||||||
|
/tmp/* | /var/folders/* | */tmp/*)
|
||||||
|
# Looks like a reasonable temp directory path
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "TEMP_DIR ($TEMP_DIR) does not look like a safe temporary directory path"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Now it's safe to remove contents - use find to avoid glob expansion issues
|
||||||
|
find "${TEMP_DIR:?}" -mindepth 1 -delete 2>/dev/null || true
|
||||||
|
unset SB_SECRET_MNEMONIC
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
echo -e "\n${YELLOW}Cleaning up...${NC}"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
unset SB_SECRET_STATE_DIR
|
||||||
|
unset SB_SECRET_MNEMONIC
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
unset GODEBUG
|
||||||
|
echo -e "${GREEN}Cleanup complete${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cleanup trap
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Check that the secret binary exists
|
||||||
|
if [ ! -f "$SECRET_BINARY" ]; then
|
||||||
|
print_error "Secret binary not found at $SECRET_BINARY. Please run 'make build' first."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 1: Set up environment variables
|
||||||
|
print_step "1" "Setting up environment variables"
|
||||||
|
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
print_success "Environment variables set"
|
||||||
|
echo " SB_SECRET_STATE_DIR=$SB_SECRET_STATE_DIR"
|
||||||
|
echo " SB_SECRET_MNEMONIC=$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Test 2: Initialize the secret manager (should create default vault)
|
||||||
|
print_step "2" "Initializing secret manager (creates default vault)"
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
echo " SB_UNLOCK_PASSPHRASE=$SB_UNLOCK_PASSPHRASE"
|
||||||
|
|
||||||
|
# Verify environment variables are exported and visible to subprocesses
|
||||||
|
echo "Verifying environment variables are exported:"
|
||||||
|
env | grep -E "^SB_" || true
|
||||||
|
|
||||||
|
echo "Running: $SECRET_BINARY init"
|
||||||
|
# Run with explicit environment to ensure variables are passed
|
||||||
|
if SB_SECRET_STATE_DIR="$SB_SECRET_STATE_DIR" \
|
||||||
|
SB_SECRET_MNEMONIC="$SB_SECRET_MNEMONIC" \
|
||||||
|
SB_UNLOCK_PASSPHRASE="$SB_UNLOCK_PASSPHRASE" \
|
||||||
|
GODEBUG="$GODEBUG" \
|
||||||
|
$SECRET_BINARY init </dev/null; then
|
||||||
|
print_success "Secret manager initialized with default vault"
|
||||||
|
else
|
||||||
|
print_error "Failed to initialize secret manager"
|
||||||
|
fi
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
|
||||||
|
# Verify directory structure was created
|
||||||
|
if [ -d "$TEMP_DIR" ]; then
|
||||||
|
print_success "State directory created: $TEMP_DIR"
|
||||||
|
else
|
||||||
|
print_error "State directory was not created"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: Vault management
|
||||||
|
print_step "3" "Testing vault management"
|
||||||
|
|
||||||
|
# List vaults (should show default)
|
||||||
|
echo "Listing vaults..."
|
||||||
|
echo "Running: $SECRET_BINARY vault list"
|
||||||
|
if $SECRET_BINARY vault list; then
|
||||||
|
VAULTS=$($SECRET_BINARY vault list)
|
||||||
|
echo "Available vaults: $VAULTS"
|
||||||
|
print_success "Listed vaults successfully"
|
||||||
|
else
|
||||||
|
print_error "Failed to list vaults"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a new vault
|
||||||
|
echo "Creating new vault 'work'..."
|
||||||
|
echo "Running: $SECRET_BINARY vault create work"
|
||||||
|
if $SECRET_BINARY vault create work; then
|
||||||
|
print_success "Created vault 'work'"
|
||||||
|
else
|
||||||
|
print_error "Failed to create vault 'work'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create another vault
|
||||||
|
echo "Creating new vault 'personal'..."
|
||||||
|
echo "Running: $SECRET_BINARY vault create personal"
|
||||||
|
if $SECRET_BINARY vault create personal; then
|
||||||
|
print_success "Created vault 'personal'"
|
||||||
|
else
|
||||||
|
print_error "Failed to create vault 'personal'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List vaults again (should show default, work, personal)
|
||||||
|
echo "Listing vaults after creation..."
|
||||||
|
echo "Running: $SECRET_BINARY vault list"
|
||||||
|
if $SECRET_BINARY vault list; then
|
||||||
|
VAULTS=$($SECRET_BINARY vault list)
|
||||||
|
echo "Available vaults: $VAULTS"
|
||||||
|
print_success "Listed vaults after creation"
|
||||||
|
else
|
||||||
|
print_error "Failed to list vaults after creation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Switch to work vault
|
||||||
|
echo "Switching to 'work' vault..."
|
||||||
|
echo "Running: $SECRET_BINARY vault select work"
|
||||||
|
if $SECRET_BINARY vault select work; then
|
||||||
|
print_success "Switched to 'work' vault"
|
||||||
|
else
|
||||||
|
print_error "Failed to switch to 'work' vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4: Import functionality with environment variable combinations
|
||||||
|
print_step "4" "Testing import functionality with environment variable combinations"
|
||||||
|
|
||||||
|
# Test 4a: Import with both env vars set (typical usage)
|
||||||
|
echo -e "\n${YELLOW}Test 4a: Import with both SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE set${NC}"
|
||||||
|
reset_state
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
|
||||||
|
# Create a vault first
|
||||||
|
echo "Running: $SECRET_BINARY vault create test-vault"
|
||||||
|
if $SECRET_BINARY vault create test-vault; then
|
||||||
|
print_success "Created test-vault for import testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create test-vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import should work without prompts
|
||||||
|
echo "Importing with both env vars set (automated)..."
|
||||||
|
echo "Running: $SECRET_BINARY vault import test-vault"
|
||||||
|
if $SECRET_BINARY vault import test-vault; then
|
||||||
|
print_success "Import succeeded with both env vars (automated)"
|
||||||
|
else
|
||||||
|
print_error "Import failed with both env vars"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4b: Import into non-existent vault (should fail)
|
||||||
|
echo -e "\n${YELLOW}Test 4b: Import into non-existent vault (should fail)${NC}"
|
||||||
|
echo "Importing into non-existent vault (should fail)..."
|
||||||
|
if $SECRET_BINARY vault import nonexistent-vault; then
|
||||||
|
print_error "Import should have failed for non-existent vault"
|
||||||
|
else
|
||||||
|
print_success "Import correctly failed for non-existent vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4c: Import with invalid mnemonic (should fail)
|
||||||
|
echo -e "\n${YELLOW}Test 4c: Import with invalid mnemonic (should fail)${NC}"
|
||||||
|
export SB_SECRET_MNEMONIC="invalid mnemonic phrase that should not work"
|
||||||
|
|
||||||
|
# Create a vault first
|
||||||
|
echo "Running: $SECRET_BINARY vault create test-vault2"
|
||||||
|
if $SECRET_BINARY vault create test-vault2; then
|
||||||
|
print_success "Created test-vault2 for invalid mnemonic testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create test-vault2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Importing with invalid mnemonic (should fail)..."
|
||||||
|
if $SECRET_BINARY vault import test-vault2; then
|
||||||
|
print_error "Import should have failed with invalid mnemonic"
|
||||||
|
else
|
||||||
|
print_success "Import correctly failed with invalid mnemonic"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reset state for remaining tests
|
||||||
|
reset_state
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Test 5: Unlocker management
|
||||||
|
print_step "5" "Testing unlocker management"
|
||||||
|
|
||||||
|
# Initialize with mnemonic and passphrase
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
echo "Running: $SECRET_BINARY init (with SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE set)"
|
||||||
|
if $SECRET_BINARY init; then
|
||||||
|
print_success "Initialized for unlocker testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to initialize for unlocker testing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create passphrase-protected unlocker
|
||||||
|
echo "Creating passphrase-protected unlocker..."
|
||||||
|
echo "Running: $SECRET_BINARY unlockers add passphrase (with SB_UNLOCK_PASSPHRASE set)"
|
||||||
|
if $SECRET_BINARY unlockers add passphrase; then
|
||||||
|
print_success "Created passphrase-protected unlocker"
|
||||||
|
else
|
||||||
|
print_error "Failed to create passphrase-protected unlocker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
|
||||||
|
# List unlockers
|
||||||
|
echo "Listing unlockers..."
|
||||||
|
echo "Running: $SECRET_BINARY unlockers list"
|
||||||
|
if $SECRET_BINARY unlockers list; then
|
||||||
|
UNLOCKERS=$($SECRET_BINARY unlockers list)
|
||||||
|
echo "Available unlockers: $UNLOCKERS"
|
||||||
|
print_success "Listed unlockers"
|
||||||
|
else
|
||||||
|
print_error "Failed to list unlockers"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 6: Secret management with mnemonic (keyless operation)
|
||||||
|
print_step "6" "Testing mnemonic-based secret operations (keyless)"
|
||||||
|
|
||||||
|
# Add secrets using mnemonic (no unlocker required)
|
||||||
|
echo "Adding secrets using mnemonic-based long-term key..."
|
||||||
|
|
||||||
|
# Test secret 1
|
||||||
|
echo "Running: echo \"my-super-secret-password\" | $SECRET_BINARY add \"database/password\""
|
||||||
|
if echo "my-super-secret-password" | $SECRET_BINARY add "database/password"; then
|
||||||
|
print_success "Added secret: database/password"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret: database/password"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test secret 2
|
||||||
|
echo "Running: echo \"api-key-12345\" | $SECRET_BINARY add \"api/key\""
|
||||||
|
if echo "api-key-12345" | $SECRET_BINARY add "api/key"; then
|
||||||
|
print_success "Added secret: api/key"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret: api/key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test secret 3 (with path)
|
||||||
|
echo "Running: echo \"ssh-private-key-content\" | $SECRET_BINARY add \"ssh/private-key\""
|
||||||
|
if echo "ssh-private-key-content" | $SECRET_BINARY add "ssh/private-key"; then
|
||||||
|
print_success "Added secret: ssh/private-key"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret: ssh/private-key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test secret 4 (with dots and underscores)
|
||||||
|
echo "Running: echo \"jwt-secret-token\" | $SECRET_BINARY add \"app.config_jwt_secret\""
|
||||||
|
if echo "jwt-secret-token" | $SECRET_BINARY add "app.config_jwt_secret"; then
|
||||||
|
print_success "Added secret: app.config_jwt_secret"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret: app.config_jwt_secret"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve secrets using mnemonic
|
||||||
|
echo "Retrieving secrets using mnemonic-based long-term key..."
|
||||||
|
|
||||||
|
# Retrieve and verify secret 1
|
||||||
|
RETRIEVED_SECRET1=$($SECRET_BINARY get "database/password" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_SECRET1" = "my-super-secret-password" ]; then
|
||||||
|
print_success "Retrieved and verified secret: database/password"
|
||||||
|
else
|
||||||
|
print_error "Failed to retrieve or verify secret: database/password"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve and verify secret 2
|
||||||
|
RETRIEVED_SECRET2=$($SECRET_BINARY get "api/key" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_SECRET2" = "api-key-12345" ]; then
|
||||||
|
print_success "Retrieved and verified secret: api/key"
|
||||||
|
else
|
||||||
|
print_error "Failed to retrieve or verify secret: api/key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve and verify secret 3
|
||||||
|
RETRIEVED_SECRET3=$($SECRET_BINARY get "ssh/private-key" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_SECRET3" = "ssh-private-key-content" ]; then
|
||||||
|
print_success "Retrieved and verified secret: ssh/private-key"
|
||||||
|
else
|
||||||
|
print_error "Failed to retrieve or verify secret: ssh/private-key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List all secrets
|
||||||
|
echo "Listing all secrets..."
|
||||||
|
echo "Running: $SECRET_BINARY list"
|
||||||
|
if $SECRET_BINARY list; then
|
||||||
|
SECRETS=$($SECRET_BINARY list)
|
||||||
|
echo "Secrets in current vault:"
|
||||||
|
echo "$SECRETS" | while read -r secret; do
|
||||||
|
echo " - $secret"
|
||||||
|
done
|
||||||
|
print_success "Listed all secrets"
|
||||||
|
else
|
||||||
|
print_error "Failed to list secrets"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 7: Secret management without mnemonic (traditional unlocker approach)
|
||||||
|
print_step "7" "Testing traditional unlocker approach"
|
||||||
|
|
||||||
|
# Create a new vault without mnemonic
|
||||||
|
echo "Running: $SECRET_BINARY vault create traditional"
|
||||||
|
$SECRET_BINARY vault create traditional
|
||||||
|
|
||||||
|
# Add a secret using traditional unlocker approach
|
||||||
|
echo "Adding secret using traditional unlocker..."
|
||||||
|
echo "Running: echo 'traditional-secret' | $SECRET_BINARY add traditional/secret"
|
||||||
|
if echo "traditional-secret" | $SECRET_BINARY add traditional/secret; then
|
||||||
|
print_success "Added secret with traditional approach"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret with traditional approach"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve secret using traditional unlocker approach
|
||||||
|
echo "Retrieving secret using traditional unlocker approach..."
|
||||||
|
echo "Running: $SECRET_BINARY get traditional/secret"
|
||||||
|
if RETRIEVED=$($SECRET_BINARY get traditional/secret 2>&1); then
|
||||||
|
print_success "Retrieved: $RETRIEVED"
|
||||||
|
else
|
||||||
|
print_error "Failed to retrieve secret with traditional approach"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 8: Advanced unlocker management
|
||||||
|
print_step "8" "Testing advanced unlocker management"
|
||||||
|
|
||||||
|
if [ "$PLATFORM" = "darwin" ]; then
|
||||||
|
# macOS only: Test Secure Enclave
|
||||||
|
echo "Testing Secure Enclave unlocker creation..."
|
||||||
|
if $SECRET_BINARY unlockers add sep; then
|
||||||
|
print_success "Created Secure Enclave unlocker"
|
||||||
|
else
|
||||||
|
print_warning "Secure Enclave unlocker creation not yet implemented"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get current unlocker ID for testing
|
||||||
|
echo "Getting current unlocker for testing..."
|
||||||
|
echo "Running: $SECRET_BINARY unlockers list"
|
||||||
|
if $SECRET_BINARY unlockers list; then
|
||||||
|
CURRENT_UNLOCKER_ID=$($SECRET_BINARY unlockers list | head -n1 | awk '{print $1}')
|
||||||
|
if [ -n "$CURRENT_UNLOCKER_ID" ]; then
|
||||||
|
print_success "Found unlocker ID: $CURRENT_UNLOCKER_ID"
|
||||||
|
|
||||||
|
# Test unlocker selection
|
||||||
|
echo "Testing unlocker selection..."
|
||||||
|
echo "Running: $SECRET_BINARY unlocker select $CURRENT_UNLOCKER_ID"
|
||||||
|
if $SECRET_BINARY unlocker select "$CURRENT_UNLOCKER_ID"; then
|
||||||
|
print_success "Selected unlocker: $CURRENT_UNLOCKER_ID"
|
||||||
|
else
|
||||||
|
print_warning "Unlocker selection not yet implemented"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 9: Secret name validation and edge cases
|
||||||
|
print_step "9" "Testing secret name validation and edge cases"
|
||||||
|
|
||||||
|
# Test valid names
|
||||||
|
VALID_NAMES=("valid-name" "valid.name" "valid_name" "valid/path/name" "123valid" "a" "very-long-name-with-many-parts/and/paths")
|
||||||
|
for name in "${VALID_NAMES[@]}"; do
|
||||||
|
echo "Running: echo \"test-value\" | $SECRET_BINARY add $name --force"
|
||||||
|
if echo "test-value" | $SECRET_BINARY add "$name" --force; then
|
||||||
|
print_success "Valid name accepted: $name"
|
||||||
|
else
|
||||||
|
print_error "Valid name rejected: $name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test invalid names (these should fail)
|
||||||
|
echo "Testing invalid names (should fail)..."
|
||||||
|
INVALID_NAMES=("Invalid-Name" "invalid name" "invalid@name" "invalid#name" "invalid%name" "")
|
||||||
|
for name in "${INVALID_NAMES[@]}"; do
|
||||||
|
echo "Running: echo \"test-value\" | $SECRET_BINARY add $name"
|
||||||
|
if echo "test-value" | $SECRET_BINARY add "$name"; then
|
||||||
|
print_error "Invalid name accepted (should have been rejected): '$name'"
|
||||||
|
else
|
||||||
|
print_success "Invalid name correctly rejected: '$name'"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test 10: Overwrite protection and force flag
|
||||||
|
print_step "10" "Testing overwrite protection and force flag"
|
||||||
|
|
||||||
|
# Try to add existing secret without --force (should fail)
|
||||||
|
echo "Running: echo \"new-value\" | $SECRET_BINARY add \"database/password\""
|
||||||
|
if echo "new-value" | $SECRET_BINARY add "database/password"; then
|
||||||
|
print_error "Overwrite protection failed - secret was overwritten without --force"
|
||||||
|
else
|
||||||
|
print_success "Overwrite protection working - secret not overwritten without --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to add existing secret with --force (should succeed)
|
||||||
|
echo "Running: echo \"new-password-value\" | $SECRET_BINARY add \"database/password\" --force"
|
||||||
|
if echo "new-password-value" | $SECRET_BINARY add "database/password" --force; then
|
||||||
|
print_success "Force overwrite working - secret overwritten with --force"
|
||||||
|
|
||||||
|
# Verify the new value
|
||||||
|
RETRIEVED_NEW=$($SECRET_BINARY get "database/password" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_NEW" = "new-password-value" ]; then
|
||||||
|
print_success "Overwritten secret has correct new value"
|
||||||
|
else
|
||||||
|
print_error "Overwritten secret has incorrect value"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Force overwrite failed - secret not overwritten with --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 11: Cross-vault operations
|
||||||
|
print_step "11" "Testing cross-vault operations"
|
||||||
|
|
||||||
|
# First create and import mnemonic into work vault since it was destroyed by reset_state
|
||||||
|
echo "Creating work vault for cross-vault testing..."
|
||||||
|
echo "Running: $SECRET_BINARY vault create work"
|
||||||
|
if $SECRET_BINARY vault create work; then
|
||||||
|
print_success "Created work vault for cross-vault testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create work vault for cross-vault testing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import mnemonic into work vault so it can store secrets
|
||||||
|
echo "Importing mnemonic into work vault..."
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
echo "Running: $SECRET_BINARY vault import work"
|
||||||
|
if $SECRET_BINARY vault import work; then
|
||||||
|
print_success "Imported mnemonic into work vault"
|
||||||
|
else
|
||||||
|
print_error "Failed to import mnemonic into work vault"
|
||||||
|
fi
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
|
||||||
|
# Switch to work vault and add secrets there
|
||||||
|
echo "Switching to 'work' vault for cross-vault testing..."
|
||||||
|
echo "Running: $SECRET_BINARY vault select work"
|
||||||
|
if $SECRET_BINARY vault select work; then
|
||||||
|
print_success "Switched to 'work' vault"
|
||||||
|
|
||||||
|
# Add work-specific secrets
|
||||||
|
echo "Running: echo \"work-database-password\" | $SECRET_BINARY add \"work/database\""
|
||||||
|
if echo "work-database-password" | $SECRET_BINARY add "work/database"; then
|
||||||
|
print_success "Added work-specific secret"
|
||||||
|
else
|
||||||
|
print_error "Failed to add work-specific secret"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List secrets in work vault
|
||||||
|
echo "Running: $SECRET_BINARY list"
|
||||||
|
if $SECRET_BINARY list; then
|
||||||
|
WORK_SECRETS=$($SECRET_BINARY list)
|
||||||
|
echo "Secrets in work vault: $WORK_SECRETS"
|
||||||
|
print_success "Listed work vault secrets"
|
||||||
|
else
|
||||||
|
print_error "Failed to list work vault secrets"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Failed to switch to 'work' vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Switch back to default vault
|
||||||
|
echo "Switching back to 'default' vault..."
|
||||||
|
echo "Running: $SECRET_BINARY vault select default"
|
||||||
|
if $SECRET_BINARY vault select default; then
|
||||||
|
print_success "Switched back to 'default' vault"
|
||||||
|
|
||||||
|
# Verify default vault secrets are still there
|
||||||
|
echo "Running: $SECRET_BINARY get \"database/password\""
|
||||||
|
if $SECRET_BINARY get "database/password"; then
|
||||||
|
print_success "Default vault secrets still accessible"
|
||||||
|
else
|
||||||
|
print_error "Default vault secrets not accessible"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Failed to switch back to 'default' vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 12: File structure verification
|
||||||
|
print_step "12" "Verifying file structure"
|
||||||
|
|
||||||
|
echo "Checking file structure in $TEMP_DIR..."
|
||||||
|
if [ -d "$TEMP_DIR/vaults.d/default/secrets.d" ]; then
|
||||||
|
print_success "Default vault structure exists"
|
||||||
|
|
||||||
|
# Check a specific secret's file structure
|
||||||
|
SECRET_DIR="$TEMP_DIR/vaults.d/default/secrets.d/database%password"
|
||||||
|
if [ -d "$SECRET_DIR" ]; then
|
||||||
|
print_success "Secret directory exists: database%password"
|
||||||
|
|
||||||
|
# Check required files for per-secret key architecture
|
||||||
|
FILES=("value.age" "pub.age" "priv.age" "secret-metadata.json")
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
if [ -f "$SECRET_DIR/$file" ]; then
|
||||||
|
print_success "Required file exists: $file"
|
||||||
|
else
|
||||||
|
print_error "Required file missing: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
print_error "Secret directory not found"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Default vault structure not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check work vault structure
|
||||||
|
if [ -d "$TEMP_DIR/vaults.d/work" ]; then
|
||||||
|
print_success "Work vault structure exists"
|
||||||
|
else
|
||||||
|
print_error "Work vault structure not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check configuration files
|
||||||
|
if [ -f "$TEMP_DIR/configuration.json" ]; then
|
||||||
|
print_success "Global configuration file exists"
|
||||||
|
else
|
||||||
|
print_warning "Global configuration file not found (may not be implemented yet)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check current vault symlink
|
||||||
|
if [ -L "$TEMP_DIR/currentvault" ] || [ -f "$TEMP_DIR/currentvault" ]; then
|
||||||
|
print_success "Current vault link exists"
|
||||||
|
else
|
||||||
|
print_error "Current vault link not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 13: Environment variable error handling
|
||||||
|
print_step "13" "Testing environment variable error handling"
|
||||||
|
|
||||||
|
# Test with non-existent state directory
|
||||||
|
export SB_SECRET_STATE_DIR="$TEMP_DIR/nonexistent/directory"
|
||||||
|
echo "Running: $SECRET_BINARY get \"database/password\""
|
||||||
|
if $SECRET_BINARY get "database/password"; then
|
||||||
|
print_error "Should have failed with non-existent state directory"
|
||||||
|
else
|
||||||
|
print_success "Correctly failed with non-existent state directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test init with non-existent directory (should work)
|
||||||
|
echo "Running: $SECRET_BINARY init (with SB_UNLOCK_PASSPHRASE set)"
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
if $SECRET_BINARY init; then
|
||||||
|
print_success "Init works with non-existent state directory"
|
||||||
|
else
|
||||||
|
print_error "Init should work with non-existent state directory"
|
||||||
|
fi
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
|
||||||
|
# Reset to working directory
|
||||||
|
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||||
|
|
||||||
|
# Test 14: Mixed approach compatibility
|
||||||
|
print_step "14" "Testing mixed approach compatibility"
|
||||||
|
|
||||||
|
# Verify mnemonic can access traditional secrets
|
||||||
|
RETRIEVED_MIXED=$($SECRET_BINARY get "traditional/secret" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_MIXED" = "traditional-secret-value" ]; then
|
||||||
|
print_success "Mnemonic can access traditional secrets"
|
||||||
|
else
|
||||||
|
print_error "Mnemonic cannot access traditional secrets"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test without mnemonic but with unlocker
|
||||||
|
echo "Testing mnemonic-created vault access..."
|
||||||
|
echo "Testing traditional unlocker access to mnemonic-created secrets..."
|
||||||
|
echo "Running: $SECRET_BINARY get test/seed (with mnemonic set)"
|
||||||
|
if RETRIEVED=$($SECRET_BINARY get test/seed 2>&1); then
|
||||||
|
print_success "Traditional unlocker can access mnemonic-created secrets"
|
||||||
|
else
|
||||||
|
print_warning "Traditional unlocker cannot access mnemonic-created secrets (may need implementation)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Re-enable mnemonic for final tests
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
echo -e "\n${GREEN}=== Test Summary ===${NC}"
|
||||||
|
echo -e "${GREEN}✓ Environment variable support (SB_SECRET_STATE_DIR, SB_SECRET_MNEMONIC)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Secret manager initialization${NC}"
|
||||||
|
echo -e "${GREEN}✓ Vault management (create, list, select)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Import functionality with environment variable combinations${NC}"
|
||||||
|
echo -e "${GREEN}✓ Import error handling (non-existent vault, invalid mnemonic)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Unlocker management (passphrase, PGP, SEP)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Secret generation and storage${NC}"
|
||||||
|
echo -e "${GREEN}✓ Traditional unlocker operations${NC}"
|
||||||
|
echo -e "${GREEN}✓ Secret name validation${NC}"
|
||||||
|
echo -e "${GREEN}✓ Overwrite protection and force flag${NC}"
|
||||||
|
echo -e "${GREEN}✓ Cross-vault operations${NC}"
|
||||||
|
echo -e "${GREEN}✓ Per-secret key file structure${NC}"
|
||||||
|
echo -e "${GREEN}✓ Mixed approach compatibility${NC}"
|
||||||
|
echo -e "${GREEN}✓ Error handling${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}🎉 Comprehensive test completed with environment variable automation!${NC}"
|
||||||
|
|
||||||
|
# Show usage examples for all implemented functionality
|
||||||
|
echo -e "\n${BLUE}=== Complete Usage Examples ===${NC}"
|
||||||
|
echo -e "${YELLOW}# Environment setup:${NC}"
|
||||||
|
echo "export SB_SECRET_STATE_DIR=\"/path/to/your/secrets\""
|
||||||
|
echo "export SB_SECRET_MNEMONIC=\"your twelve word mnemonic phrase here\""
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Initialization:${NC}"
|
||||||
|
echo "secret init"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Vault management:${NC}"
|
||||||
|
echo "secret vault list"
|
||||||
|
echo "secret vault create work"
|
||||||
|
echo "secret vault select work"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Import mnemonic (automated with environment variables):${NC}"
|
||||||
|
echo "export SB_SECRET_MNEMONIC=\"abandon abandon...\""
|
||||||
|
echo "export SB_UNLOCK_PASSPHRASE=\"passphrase\""
|
||||||
|
echo "secret vault import work"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Unlocker management:${NC}"
|
||||||
|
echo "$SECRET_BINARY unlockers add <type> # Add unlocker (passphrase, pgp, keychain)"
|
||||||
|
echo "$SECRET_BINARY unlockers add passphrase"
|
||||||
|
echo "$SECRET_BINARY unlockers add pgp <gpg-key-id>"
|
||||||
|
echo "$SECRET_BINARY unlockers add keychain # macOS only"
|
||||||
|
echo "$SECRET_BINARY unlockers list # List all unlockers"
|
||||||
|
echo "$SECRET_BINARY unlocker select <unlocker-id> # Select current unlocker"
|
||||||
|
echo "$SECRET_BINARY unlockers rm <unlocker-id> # Remove unlocker"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Secret management:${NC}"
|
||||||
|
echo "echo \"my-secret\" | secret add \"app/password\""
|
||||||
|
echo "echo \"my-secret\" | secret add \"app/password\" --force"
|
||||||
|
echo "secret get \"app/password\""
|
||||||
|
echo "secret list"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Cross-vault operations:${NC}"
|
||||||
|
echo "secret vault select work"
|
||||||
|
echo "echo \"work-secret\" | secret add \"work/database\""
|
||||||
|
echo "secret vault select default"
|
Loading…
Reference in New Issue
Block a user