Author SHA1 Message Date
clawbot df47ab386c Test only small secrets in the size tests (closes #52)
check / check (push) Waiting to run
The size tests for secret add, secret import and the stdin buffer no
longer store 2 MB to 101 MB secrets; the largest is 1 MiB. The cases
checking that a secret over the 100 MB limit is rejected are deleted
and not replaced. The limit itself is unchanged. With nothing large
left, the helper that skipped a case for want of locked memory is gone.

Model: opus-5-5
2026-10-06 02:22:38 +02:00
8 changed files with 24 additions and 149 deletions
+6
View File
@@ -18,6 +18,12 @@ https://git.eeqj.de/sneak/secret/milestone/12
# Completed Steps
- 2026-10-05: No test stores a secret larger than 1 MiB
(https://git.eeqj.de/sneak/secret/issues/52). The size tests for `secret add`,
`secret import` and the stdin buffer no longer try 2 MB, 10 MB, 99 MB, 100 MB
or 101 MB secrets, and nothing tests that a secret over the 100 MB limit is
rejected; the limit itself is unchanged. With nothing large left, the size
tests no longer skip a case for want of locked memory.
- 2026-10-05: The Go module path is `sneak.berlin/go/secret`, as
`REPO_POLICIES.md` requires, not `git.eeqj.de/sneak/secret`
(https://git.eeqj.de/sneak/secret/issues/43). Every import uses it, as do the
+2 -2
View File
@@ -353,9 +353,9 @@ func TestRemovalWithoutTerminalFailsAtOnce(t *testing.T) {
// for its answer, another command can take the state directory lock and
// change the secret, and that the removal then removes nothing, since the
// secret is no longer what the question named.
//
//nolint:paralleltest // times commands against the in-memory lock all tests share
func TestRemovalAsksWithoutHoldingLock(t *testing.T) {
t.Parallel()
r := newRemoval(t, "rm")
answers, answerWriter := io.Pipe()
+1 -5
View File
@@ -52,12 +52,8 @@ func runSecretWithStdin(stdin string, env map[string]string, args ...string) (st
return cli.ExecuteCommandInProcess(args, stdin, env)
}
// TestMain runs before all tests and ensures the binary is built. It also
// makes passphrase encryption in the tests cheap (see
// secret.ScryptWorkFactor); the binary keeps age's work factor.
// TestMain runs before all tests and ensures the binary is built
func TestMain(m *testing.M) {
secret.ScryptWorkFactor = 1
// Get the current working directory
wd, err := os.Getwd()
if err != nil {
+2 -2
View File
@@ -235,9 +235,9 @@ func TestEncryptPipedIntoAdd(t *testing.T) {
// TestFailedCommandReleasesLock checks that a command failing after it
// took the state directory lock leaves the lock free for the next command.
//
//nolint:paralleltest // times commands against the in-memory lock all tests share
func TestFailedCommandReleasesLock(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
cli := NewCLIInstanceWithStateDir(fs, testStateDir)
+13 -108
View File
@@ -14,7 +14,6 @@ import (
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
"sneak.berlin/go/secret/internal/vault"
"sneak.berlin/go/secret/pkg/agehd"
)
@@ -22,45 +21,6 @@ import (
// testVaultName is the vault name used by the size tests.
const testVaultName = "test-vault"
// lockedBytesPerSecretByte bounds the locked memory that storing a secret
// holds at once: the buffers it is read into reach up to 1.5 times its
// size, and they are then copied into one more buffer of its size.
const lockedBytesPerSecretByte = 3
// skipIfLockedMemoryTooLow skips the test when this process cannot lock
// the memory a secret of size bytes needs, found by locking a buffer of
// that size and releasing it. memguard panics, ending the whole test run,
// when it cannot lock a buffer, and a plain `docker build .` runs the
// tests under an 8 MiB locked-memory limit (RLIMIT_MEMLOCK). A process
// allowed to lock past that limit runs every case.
func skipIfLockedMemoryTooLow(t *testing.T, size int) {
t.Helper()
need := lockedBytesPerSecretByte * size
buf, err := unix.Mmap(-1, 0, need,
unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANON)
require.NoError(t, err)
lockErr := unix.Mlock(buf)
// Unmapping the buffer also unlocks it.
err = unix.Munmap(buf)
require.NoError(t, err)
if lockErr != nil {
var limit unix.Rlimit
err = unix.Getrlimit(unix.RLIMIT_MEMLOCK, &limit)
require.NoError(t, err)
t.Skipf("a %d-byte secret needs up to %d bytes of locked memory, "+
"which could not be locked under the locked-memory limit "+
"(RLIMIT_MEMLOCK) of %d bytes: %v",
size, need, limit.Cur, lockErr)
}
}
// newSizeTestVault creates an in-memory vault unlocked with the test
// mnemonic and returns the filesystem and vault.
//
@@ -93,10 +53,9 @@ func newSizeTestVault(t *testing.T) (afero.Fs, *vault.Vault) {
}
// runAddSecretSizeCase adds a secret of the given size through stdin and
// verifies the outcome: wantErr, or the secret stored when wantErr is nil.
func runAddSecretSizeCase(t *testing.T, size int, wantErr error) {
// verifies that it is stored.
func runAddSecretSizeCase(t *testing.T, size int) {
t.Helper()
skipIfLockedMemoryTooLow(t, size)
fs, vlt := newSizeTestVault(t)
@@ -127,13 +86,6 @@ func runAddSecretSizeCase(t *testing.T, size int, wantErr error) {
// Test adding the secret
secretName := fmt.Sprintf("test-secret-%d", size)
err = cli.AddSecret(secretName, false)
if wantErr != nil {
require.ErrorIs(t, err, wantErr)
return
}
require.NoError(t, err)
// Verify the secret was stored correctly
@@ -147,10 +99,9 @@ func runAddSecretSizeCase(t *testing.T, size int, wantErr error) {
}
// runImportSecretSizeCase imports a secret file of the given size and
// verifies the outcome: wantErr, or the secret stored when wantErr is nil.
func runImportSecretSizeCase(t *testing.T, size int, wantErr error) {
// verifies that it is stored.
func runImportSecretSizeCase(t *testing.T, size int) {
t.Helper()
skipIfLockedMemoryTooLow(t, size)
fs, vlt := newSizeTestVault(t)
@@ -179,13 +130,6 @@ func runImportSecretSizeCase(t *testing.T, size int, wantErr error) {
// Test importing the secret
secretName := fmt.Sprintf("imported-secret-%d", size)
err = cli.ImportSecret(cmd, secretName, testFile, false)
if wantErr != nil {
require.ErrorIs(t, err, wantErr)
return
}
require.NoError(t, err)
// Verify the secret was stored correctly
@@ -200,12 +144,11 @@ func runImportSecretSizeCase(t *testing.T, size int, wantErr error) {
// TestAddSecretVariousSizes tests adding secrets of various sizes through stdin
//
//nolint:paralleltest // together the subtests lock more than the memlock limit
//nolint:paralleltest // in parallel, size tests could exceed the memlock limit
func TestAddSecretVariousSizes(t *testing.T) {
tests := []struct {
name string
size int
wantErr error
name string
size int
}{
{
name: "1KB secret",
@@ -223,40 +166,22 @@ func TestAddSecretVariousSizes(t *testing.T) {
name: "1MB secret",
size: 1024 * 1024,
},
{
name: "10MB secret",
size: 10 * 1024 * 1024,
},
{
name: "99MB secret",
size: 99 * 1024 * 1024,
},
{
name: "100MB secret minus 1 byte",
size: 100*1024*1024 - 1,
},
{
name: "101MB secret - should fail",
size: 101 * 1024 * 1024,
wantErr: errSecretTooLarge,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runAddSecretSizeCase(t, tt.size, tt.wantErr)
runAddSecretSizeCase(t, tt.size)
})
}
}
// TestImportSecretVariousSizes tests importing secrets of various sizes from files
//
//nolint:paralleltest // together the subtests lock more than the memlock limit
//nolint:paralleltest // in parallel, size tests could exceed the memlock limit
func TestImportSecretVariousSizes(t *testing.T) {
tests := []struct {
name string
size int
wantErr error
name string
size int
}{
{
name: "1KB file",
@@ -274,35 +199,18 @@ func TestImportSecretVariousSizes(t *testing.T) {
name: "1MB file",
size: 1024 * 1024,
},
{
name: "10MB file",
size: 10 * 1024 * 1024,
},
{
name: "99MB file",
size: 99 * 1024 * 1024,
},
{
name: "100MB file",
size: 100 * 1024 * 1024,
},
{
name: "101MB file - should fail",
size: 101 * 1024 * 1024,
wantErr: errSecretTooLarge,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runImportSecretSizeCase(t, tt.size, tt.wantErr)
runImportSecretSizeCase(t, tt.size)
})
}
}
// TestAddSecretBufferGrowth tests that our buffer growth strategy works correctly
//
//nolint:paralleltest // together the subtests lock more than the memlock limit
//nolint:paralleltest // in parallel, size tests could exceed the memlock limit
func TestAddSecretBufferGrowth(t *testing.T) {
// Test various sizes that should trigger buffer growth
sizes := []int{
@@ -321,13 +229,10 @@ func TestAddSecretBufferGrowth(t *testing.T) {
131072, // 128KB
524288, // 512KB
1048576, // 1MB
2097152, // 2MB
}
for _, size := range sizes {
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
skipIfLockedMemoryTooLow(t, size)
fs, vlt := newSizeTestVault(t)
// Create test data of exactly the specified size
-15
View File
@@ -28,17 +28,6 @@ var (
// terminal to read the mnemonic from, reading it failed, or it was empty.
var ErrMnemonicNotRead = errors.New("failed to read mnemonic")
// ScryptWorkFactor is, when not zero, the scrypt work factor that
// EncryptWithPassphrase uses instead of age's, 18: log2 of scrypt's cost
// parameter N. Deriving a key with age's takes about a second and 256 MiB, on
// purpose, since so does every guess at the passphrase. Only tests set it,
// lower, before any test runs, so that the passphrase unlockers they create
// cost nothing; the program leaves it zero. Decryption takes the work factor
// from the encrypted data, so it needs no setting.
//
//nolint:gochecknoglobals // set by the tests of the packages that use this one
var ScryptWorkFactor int
// EncryptToRecipient encrypts data to a recipient using age
// The data parameter should be a LockedBuffer for secure memory handling
func EncryptToRecipient(
@@ -153,10 +142,6 @@ func EncryptWithPassphrase(
return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
}
if ScryptWorkFactor != 0 {
recipient.SetWorkFactor(ScryptWorkFactor)
}
return EncryptToRecipient(data, recipient)
}
-8
View File
@@ -25,14 +25,6 @@ var (
errNotImplementedInMock = errors.New("not implemented in mock")
)
// TestMain makes passphrase encryption in the tests cheap; see
// ScryptWorkFactor.
func TestMain(m *testing.M) {
ScryptWorkFactor = 1
os.Exit(m.Run())
}
// MockVault is a test implementation of the VaultInterface
type MockVault struct {
name string
-9
View File
@@ -3,7 +3,6 @@ package vault_test
import (
"bytes"
"errors"
"os"
"path/filepath"
"slices"
"testing"
@@ -29,14 +28,6 @@ const (
testPassphrase = "test-passphrase"
)
// TestMain makes passphrase encryption in the tests cheap; see
// secret.ScryptWorkFactor.
func TestMain(m *testing.M) {
secret.ScryptWorkFactor = 1
os.Exit(m.Run())
}
// testMnemonicBuffer returns testMnemonic in a locked buffer that is
// destroyed when the test ends.
func testMnemonicBuffer(t *testing.T) *memguard.LockedBuffer {