Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dfa13a76f |
@@ -144,25 +144,20 @@ func TestVaultCreationLeavesNoSecretInEnvironment(t *testing.T) {
|
||||
// TestNoVaultImportCommand is a regression test for
|
||||
// https://git.eeqj.de/sneak/secret/issues/139: `secret vault import`
|
||||
// refused every vault that `secret vault create` makes, because each has a
|
||||
// long-term key. The command is gone, so cobra answers it with the help of
|
||||
// `secret vault`, as it does any subcommand of `secret vault` it does not
|
||||
// know.
|
||||
// long-term key. The command is gone, so it fails as any subcommand of
|
||||
// `secret vault` that does not exist does.
|
||||
func TestNoVaultImportCommand(t *testing.T) {
|
||||
t.Setenv(secret.EnvStateDir, t.TempDir())
|
||||
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
||||
t.Setenv(secret.EnvUnlockPassphrase, testPassphrase)
|
||||
|
||||
run := func(args ...string) string {
|
||||
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
||||
t.Setenv(secret.EnvUnlockPassphrase, testPassphrase)
|
||||
_, err := cli.ExecuteCommandInProcess(
|
||||
strings.Fields("vault create work"), "", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
output, err := cli.ExecuteCommandInProcess(args, "", nil)
|
||||
require.NoError(t, err, "%v", args)
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
run("vault", "create", "work")
|
||||
|
||||
require.Equal(t, run("vault"), run("vault", "import", "work"))
|
||||
_, err = cli.ExecuteCommandInProcess(
|
||||
strings.Fields("vault import work"), "", nil)
|
||||
require.EqualError(t, err, `unknown command "import" for "secret vault"`)
|
||||
}
|
||||
|
||||
// TestStopAtPassphrasePromptLeavesNothing is a regression test for the
|
||||
|
||||
@@ -29,6 +29,12 @@ func newGenerateCmd() *cobra.Command {
|
||||
Use: "generate",
|
||||
Short: "Generate random data",
|
||||
Long: `Generate various types of random data including mnemonics and secrets.`,
|
||||
// Cobra checks Args only on a command that has a RunE; without
|
||||
// one, an unknown subcommand prints this help and exits 0.
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(newGenerateMnemonicCmd())
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/secret/internal/secret"
|
||||
)
|
||||
|
||||
// TestUnknownSubcommandFails is a regression test for
|
||||
// https://git.eeqj.de/sneak/secret/issues/183: given a subcommand it does not
|
||||
// have, `secret vault`, `secret unlocker`, `secret version` and `secret
|
||||
// generate` printed their help and exited 0, so a script could not tell a
|
||||
// mistyped command from a successful one. Each runs here as the built binary
|
||||
// and must exit 1 naming the subcommand, as an unknown command does at the
|
||||
// top level, and must still print its help and exit 0 when given nothing.
|
||||
func TestUnknownSubcommandFails(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stateDir := t.TempDir()
|
||||
|
||||
// run runs the built binary with args and returns what it printed and
|
||||
// its exit code
|
||||
run := func(args ...string) (string, int) {
|
||||
//nolint:gosec // G204: test executes the freshly built secret binary
|
||||
cmd := exec.CommandContext(t.Context(), secretBinaryPath(t), args...)
|
||||
cmd.Env = []string{secret.EnvStateDir + "=" + stateDir}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
require.ErrorAs(t, err, &exitErr, "secret %v did not run", args)
|
||||
}
|
||||
|
||||
return string(output), cmd.ProcessState.ExitCode()
|
||||
}
|
||||
|
||||
for _, call := range []string{
|
||||
"vault improt", "unlocker foo", "version foo", "generate foo",
|
||||
} {
|
||||
parent, unknown, _ := strings.Cut(call, " ")
|
||||
|
||||
output, exitCode := run(parent, unknown)
|
||||
assert.Equal(t, 1, exitCode, "secret %s should fail:\n%s", call, output)
|
||||
assert.Contains(t, output, fmt.Sprintf(
|
||||
"unknown command %q for \"secret %s\"", unknown, parent))
|
||||
|
||||
output, exitCode = run(parent)
|
||||
assert.Equal(t, 0, exitCode,
|
||||
"secret %s should succeed:\n%s", parent, output)
|
||||
assert.Contains(t, output, usageHeading,
|
||||
"secret %s should print its help", parent)
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,12 @@ func newUnlockerCmd() *cobra.Command {
|
||||
Use: "unlocker",
|
||||
Short: "Manage unlockers",
|
||||
Long: `Create, list, and remove unlockers for the current vault.`,
|
||||
// Cobra checks Args only on a command that has a RunE; without
|
||||
// one, an unknown subcommand prints this help and exits 0.
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(newUnlockerListCmd())
|
||||
|
||||
@@ -29,6 +29,12 @@ func newVaultCmd() *cobra.Command {
|
||||
Use: "vault",
|
||||
Short: "Manage vaults",
|
||||
Long: `Create, list, and select vaults for organizing secrets.`,
|
||||
// Cobra checks Args only on a command that has a RunE; without
|
||||
// one, an unknown subcommand prints this help and exits 0.
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(newVaultListCmd())
|
||||
|
||||
@@ -43,6 +43,12 @@ func VersionCommands(cli *Instance) *cobra.Command {
|
||||
Short: "Manage secret versions",
|
||||
Long: "Commands for managing secret versions including listing, " +
|
||||
"promoting, and retrieving specific versions",
|
||||
// Cobra checks Args only on a command that has a RunE; without
|
||||
// one, an unknown subcommand prints this help and exits 0.
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
// List versions command
|
||||
|
||||
@@ -388,8 +388,7 @@ func ResolveGPGKeyFingerprint(keyID string) (string, error) {
|
||||
return "", fmt.Errorf("%w: %s", ErrGPGKeyNotFound, keyID)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf(
|
||||
"failed to resolve GPG key fingerprint: %w", withGPGStderr(err))
|
||||
return "", fmt.Errorf("failed to resolve GPG key fingerprint: %w", err)
|
||||
}
|
||||
|
||||
// Parse the output to extract the fingerprint
|
||||
@@ -419,24 +418,6 @@ func checkGPGAvailable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// withGPGStderr appends gpg's stderr, which says why gpg failed, to err
|
||||
// from cmd.Output. cmd.Output keeps that stderr in the *exec.ExitError.
|
||||
// err is returned unchanged when gpg wrote nothing to stderr.
|
||||
func withGPGStderr(err error) error {
|
||||
var exitErr *exec.ExitError
|
||||
|
||||
if !errors.As(err, &exitErr) {
|
||||
return err
|
||||
}
|
||||
|
||||
stderr := strings.TrimSpace(string(exitErr.Stderr))
|
||||
if stderr == "" {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s", err, stderr)
|
||||
}
|
||||
|
||||
// gpgEncryptDefault is the default implementation of GPG encryption
|
||||
func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error) {
|
||||
if data == nil {
|
||||
@@ -456,7 +437,7 @@ func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GPG encryption failed: %w", withGPGStderr(err))
|
||||
return nil, fmt.Errorf("GPG encryption failed: %w", err)
|
||||
}
|
||||
|
||||
return output, nil
|
||||
@@ -469,7 +450,7 @@ func gpgDecryptDefault(encryptedData []byte) (*memguard.LockedBuffer, error) {
|
||||
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GPG decryption failed: %w", withGPGStderr(err))
|
||||
return nil, fmt.Errorf("GPG decryption failed: %w", err)
|
||||
}
|
||||
|
||||
// Create a secure buffer for the decrypted data
|
||||
|
||||
@@ -102,27 +102,3 @@ func TestPGPUnlockerAddedTwiceKeepsFirst(t *testing.T) {
|
||||
assert.NotEqual(t, first.GetDirectory(), second.GetDirectory())
|
||||
assert.Equal(t, firstFiles, dirFiles(t, fs, first.GetDirectory()))
|
||||
}
|
||||
|
||||
// TestGPGDecryptErrorHasGPGReason runs the real gpg on data that is not an
|
||||
// OpenPGP message and checks that the error carries the reason gpg writes
|
||||
// to stderr.
|
||||
func TestGPGDecryptErrorHasGPGReason(t *testing.T) {
|
||||
// The empty GNUPGHOME keeps gpg away from the user's own keyring;
|
||||
// LC_ALL=C keeps gpg's message in English.
|
||||
t.Setenv("GNUPGHOME", t.TempDir())
|
||||
t.Setenv("LC_ALL", "C")
|
||||
|
||||
_, err := secret.GPGDecryptFunc([]byte("not an OpenPGP message"))
|
||||
require.ErrorContains(t, err, "gpg: no valid OpenPGP data found")
|
||||
}
|
||||
|
||||
// TestGPGDecryptErrorWithoutGPGStderr checks that when gpg fails without
|
||||
// writing to stderr, the error ends with the exit status and nothing after it.
|
||||
//
|
||||
//nolint:paralleltest // installFakeGPG uses t.Setenv
|
||||
func TestGPGDecryptErrorWithoutGPGStderr(t *testing.T) {
|
||||
installFakeGPG(t)
|
||||
|
||||
_, err := secret.GPGDecryptFunc([]byte("not an OpenPGP message"))
|
||||
require.EqualError(t, err, "GPG decryption failed: exit status 1")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user