diff --git a/internal/cli/unlockers.go b/internal/cli/unlockers.go index 87b3d7e..2e1e3de 100644 --- a/internal/cli/unlockers.go +++ b/internal/cli/unlockers.go @@ -314,7 +314,8 @@ func (cli *Instance) UnlockersList(jsonOutput bool) error { return err } - var unlockers []UnlockerInfo + // Not nil, so that a vault with no unlockers prints [] in JSON, not null + unlockers := make([]UnlockerInfo, 0, len(unlockerMetadata)) for _, unlockerID := range slices.Sorted(maps.Keys(unlockerMetadata)) { metadata := unlockerMetadata[unlockerID] diff --git a/internal/cli/unlockers_list_test.go b/internal/cli/unlockers_list_test.go index 0d72990..0d082d6 100644 --- a/internal/cli/unlockers_list_test.go +++ b/internal/cli/unlockers_list_test.go @@ -8,6 +8,8 @@ // - TestUnlockersListSkipsUnreadableMetadata: an unlocker whose metadata // file cannot be checked for or read is left out, and the other is // still listed. +// - TestUnlockersListEmptyJSON: a vault with no unlockers lists them as +// an empty JSON list. //nolint:testpackage // white-box test of unexported internals package cli @@ -321,3 +323,36 @@ func TestUnlockersListSkipsUnreadableMetadata(t *testing.T) { }) } } + +// TestUnlockersListEmptyJSON is a regression test for +// https://git.eeqj.de/sneak/secret/issues/148: listing the unlockers of a +// vault that has none, as after its last unlocker is removed, printed +// "unlockers": null in JSON form instead of an empty list. +func TestUnlockersListEmptyJSON(t *testing.T) { + t.Parallel() + + fs := afero.NewMemMapFs() + vaultDir := filepath.Join(listTestStateDir, "vaults.d", listTestVaultName) + + require.NoError(t, afero.WriteFile( + fs, filepath.Join(listTestStateDir, "currentvault"), + []byte(listTestVaultName), listTestFilePerm, + )) + require.NoError(t, fs.MkdirAll( + filepath.Join(vaultDir, listTestUnlockersDirName), listTestDirPerm, + )) + + var buf bytes.Buffer + + cmd := &cobra.Command{} + cmd.SetOut(&buf) + + instance := &Instance{fs: fs, stateDir: listTestStateDir, cmd: cmd} + require.NoError(t, instance.UnlockersList(true)) + + var decoded map[string]any + + require.NoError(t, json.Unmarshal(buf.Bytes(), &decoded)) + assert.Equal(t, []any{}, decoded["unlockers"], + "no unlockers must be printed as [], not %s", buf.String()) +} diff --git a/internal/cli/vault_list_test.go b/internal/cli/vault_list_test.go new file mode 100644 index 0000000..8a216dd --- /dev/null +++ b/internal/cli/vault_list_test.go @@ -0,0 +1,42 @@ +package cli_test + +import ( + "bytes" + "encoding/json" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sneak.berlin/go/secret/internal/cli" + "sneak.berlin/go/secret/internal/secret" +) + +// TestVaultListEmptyJSON is a regression test for +// https://git.eeqj.de/sneak/secret/issues/148: listing the vaults when +// vaults.d holds none printed "vaults": null in JSON form instead of an +// empty list. +func TestVaultListEmptyJSON(t *testing.T) { + t.Parallel() + + fs := afero.NewMemMapFs() + require.NoError(t, fs.MkdirAll( + filepath.Join(testStateDir, "vaults.d"), secret.DirPerms, + )) + + var buf bytes.Buffer + + cmd := &cobra.Command{} + cmd.SetOut(&buf) + + c := cli.NewCLIInstanceWithStateDir(fs, testStateDir) + require.NoError(t, c.ListVaults(cmd, true)) + + var decoded map[string]any + + require.NoError(t, json.Unmarshal(buf.Bytes(), &decoded)) + assert.Equal(t, []any{}, decoded["vaults"], + "no vaults must be printed as [], not %s", buf.String()) +} diff --git a/internal/vault/management.go b/internal/vault/management.go index 5bf9f3f..26d894f 100644 --- a/internal/vault/management.go +++ b/internal/vault/management.go @@ -140,8 +140,9 @@ func ListVaults(fs afero.Fs, stateDir string) ([]string, error) { return nil, fmt.Errorf("failed to read vaults directory: %w", err) } - // Extract vault names - var vaults []string + // Extract vault names. Not nil, so that no vaults prints [] in JSON, + // not null. + vaults := make([]string, 0, len(entries)) for _, entry := range entries { if entry.IsDir() {