Count every file of each version in secret info's Total Size (closes #147)
check / check (push) Waiting to run
check / check (push) Waiting to run
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
This commit is contained in:
+12
-13
@@ -18,24 +18,23 @@ type vaultStats struct {
|
|||||||
|
|
||||||
// addVersion accumulates size and timestamp info for one version directory
|
// addVersion accumulates size and timestamp info for one version directory
|
||||||
func (s *vaultStats) addVersion(fs afero.Fs, versionPath string) {
|
func (s *vaultStats) addVersion(fs afero.Fs, versionPath string) {
|
||||||
// Add size of encrypted data
|
// Add the size of every file in the version directory: its value,
|
||||||
dataPath := filepath.Join(versionPath, "data.age")
|
// metadata and keys. A directory that cannot be read adds nothing and
|
||||||
|
// gets a warning; its timestamp is still tracked below.
|
||||||
stat, err := fs.Stat(dataPath)
|
files, err := afero.ReadDir(fs, versionPath)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
s.totalSize += stat.Size()
|
secret.Warn("Could not read version directory",
|
||||||
|
"directory", versionPath, "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add size of metadata
|
for _, file := range files {
|
||||||
metaPath := filepath.Join(versionPath, "metadata.age")
|
if !file.IsDir() {
|
||||||
|
s.totalSize += file.Size()
|
||||||
stat, err = fs.Stat(metaPath)
|
}
|
||||||
if err == nil {
|
|
||||||
s.totalSize += stat.Size()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track timestamps
|
// Track timestamps
|
||||||
stat, err = fs.Stat(versionPath)
|
stat, err := fs.Stat(versionPath)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
modTime := stat.ModTime()
|
modTime := stat.ModTime()
|
||||||
if s.oldestTime.IsZero() || modTime.Before(s.oldestTime) {
|
if s.oldestTime.IsZero() || modTime.Before(s.oldestTime) {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user