From 220213abfefceb0e4758be41c6d350d281a5b19e Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Thu, 15 Oct 2026 03:15:04 +0200 Subject: [PATCH] Count every file of each version in secret info's Total Size (closes #147) Total Size added up each version's data.age and metadata.age, but a version keeps its value in value.age, so no secret's value was counted. It now adds up every file in each version directory: the value, the metadata and the version's two keys, as the plan on the issue decided. A version directory that cannot be read adds nothing and gets a warning naming it. The new tests add a 10000-byte secret and check that the total grows by at least that much, and check the warning for a version directory that cannot be read. Model: opus-5-5 --- internal/cli/info_helper.go | 25 ++++++------- internal/cli/info_helper_test.go | 51 +++++++++++++++++++++++++ internal/cli/info_test.go | 64 ++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 internal/cli/info_helper_test.go create mode 100644 internal/cli/info_test.go diff --git a/internal/cli/info_helper.go b/internal/cli/info_helper.go index 35e90ab..efbca8e 100644 --- a/internal/cli/info_helper.go +++ b/internal/cli/info_helper.go @@ -18,24 +18,23 @@ type vaultStats struct { // addVersion accumulates size and timestamp info for one version directory func (s *vaultStats) addVersion(fs afero.Fs, versionPath string) { - // Add size of encrypted data - dataPath := filepath.Join(versionPath, "data.age") - - stat, err := fs.Stat(dataPath) - if err == nil { - s.totalSize += stat.Size() + // Add the size of every file in the version directory: its value, + // metadata and keys. A directory that cannot be read adds nothing and + // gets a warning; its timestamp is still tracked below. + files, err := afero.ReadDir(fs, versionPath) + if err != nil { + secret.Warn("Could not read version directory", + "directory", versionPath, "error", err) } - // Add size of metadata - metaPath := filepath.Join(versionPath, "metadata.age") - - stat, err = fs.Stat(metaPath) - if err == nil { - s.totalSize += stat.Size() + for _, file := range files { + if !file.IsDir() { + s.totalSize += file.Size() + } } // Track timestamps - stat, err = fs.Stat(versionPath) + stat, err := fs.Stat(versionPath) if err == nil { modTime := stat.ModTime() if s.oldestTime.IsZero() || modTime.Before(s.oldestTime) { diff --git a/internal/cli/info_helper_test.go b/internal/cli/info_helper_test.go new file mode 100644 index 0000000..ca5f7ab --- /dev/null +++ b/internal/cli/info_helper_test.go @@ -0,0 +1,51 @@ +//nolint:testpackage // white-box test of unexported internals +package cli + +import ( + "io" + "os" + "path/filepath" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestAddVersionWarnsWhenVersionDirUnreadable asserts that a version +// directory that cannot be listed, so that its files are left out of the +// total size, gets a warning naming it and the error, and that its +// timestamp is still tracked. +// +//nolint:paralleltest // replaces os.Stderr, where secret.Warn writes +func TestAddVersionWarnsWhenVersionDirUnreadable(t *testing.T) { + versionDir := filepath.Join(testVaultDir(listTestVaultName), + unreadableTestSecretsDirName, unreadableTestSecretName, + "versions", "20260809.001") + + base := afero.NewMemMapFs() + require.NoError(t, base.MkdirAll(versionDir, listTestDirPerm)) + + stderrReader, stderrWriter, err := os.Pipe() + require.NoError(t, err) + + defer func() { _ = stderrReader.Close() }() + + realStderr := os.Stderr + os.Stderr = stderrWriter + + var stats vaultStats + stats.addVersion(&openFailFs{Fs: base, path: versionDir}, versionDir) + + os.Stderr = realStderr + + require.NoError(t, stderrWriter.Close()) + + warning, err := io.ReadAll(stderrReader) + require.NoError(t, err) + + assert.Contains(t, string(warning), versionDir) + assert.Contains(t, string(warning), errOpenFailed.Error()) + assert.False(t, stats.latestTime.IsZero(), + "the timestamp must still be tracked") +} diff --git a/internal/cli/info_test.go b/internal/cli/info_test.go new file mode 100644 index 0000000..5c5def2 --- /dev/null +++ b/internal/cli/info_test.go @@ -0,0 +1,64 @@ +package cli_test + +import ( + "bytes" + "crypto/rand" + "encoding/json" + "testing" + + "github.com/awnumar/memguard" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "sneak.berlin/go/secret/internal/cli" + "sneak.berlin/go/secret/internal/vault" +) + +// TestInfoTotalSizeCountsSecretValues is a regression test for +// https://git.eeqj.de/sneak/secret/issues/147, where the total size shown by +// `secret info` left out every secret's value. Adding a secret must make it +// grow by at least the size of the value. +func TestInfoTotalSizeCountsSecretValues(t *testing.T) { + t.Parallel() + + const valueSize = 10000 + + fs := afero.NewMemMapFs() + + vlt, err := vault.CreateVault(fs, testStateDir, "default", + testMnemonicBuffer(t), nil) + require.NoError(t, err) + + before := infoTotalSize(t, fs) + + valueBytes := make([]byte, valueSize) + _, err = rand.Read(valueBytes) + require.NoError(t, err) + + value := memguard.NewBufferFromBytes(valueBytes) + defer value.Destroy() + + require.NoError(t, vlt.AddSecret("s1", value, false)) + + after := infoTotalSize(t, fs) + require.GreaterOrEqual(t, after-before, int64(valueSize)) +} + +// infoTotalSize returns the total size that `secret info --json` reports for +// the state directory on fs. +func infoTotalSize(t *testing.T, fs afero.Fs) int64 { + t.Helper() + + var out bytes.Buffer + + cmd := &cobra.Command{} + cmd.SetOut(&out) + + err := cli.NewCLIInstanceWithStateDir(fs, testStateDir).Info(cmd, true) + require.NoError(t, err) + + var info cli.InfoOutput + require.NoError(t, json.Unmarshal(out.Bytes(), &info)) + + return info.TotalSize +}