//nolint:testpackage // white-box test of unexported internals package cli import ( "bytes" "crypto/rand" "fmt" "io" "path/filepath" "strings" "testing" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "git.eeqj.de/sneak/secret/pkg/agehd" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // testVaultName is the vault name used by the size tests. const testVaultName = "test-vault" // newSizeTestVault creates an in-memory vault unlocked with the test // mnemonic and returns the filesystem and vault. // //nolint:ireturn // afero.Fs is the filesystem abstraction used throughout func newSizeTestVault(t *testing.T) (afero.Fs, *vault.Vault) { t.Helper() fs := afero.NewMemMapFs() // Set test mnemonic t.Setenv(secret.EnvMnemonic, testMnemonic) // Create vault _, err := vault.CreateVault(fs, testStateDir, testVaultName) require.NoError(t, err) // Set current vault currentVaultPath := filepath.Join(testStateDir, "currentvault") vaultPath := filepath.Join(testStateDir, "vaults.d", testVaultName) err = afero.WriteFile(fs, currentVaultPath, []byte(vaultPath), 0o600) require.NoError(t, err) // Get vault and set up long-term key vlt, err := vault.GetCurrentVault(fs, testStateDir) require.NoError(t, err) ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0) require.NoError(t, err) vlt.Unlock(ltIdentity) return fs, vlt } // runAddSecretSizeCase adds a secret of the given size through stdin and // verifies the outcome. func runAddSecretSizeCase(t *testing.T, size int, wantErr bool, errMsg string) { t.Helper() fs, vlt := newSizeTestVault(t) // Generate test data of specified size testData := make([]byte, size) _, err := rand.Read(testData) require.NoError(t, err) // Add newline that will be stripped testDataWithNewline := make([]byte, 0, len(testData)+1) testDataWithNewline = append(testDataWithNewline, testData...) testDataWithNewline = append(testDataWithNewline, '\n') // Create command with fake stdin cmd := &cobra.Command{} cmd.SetIn(bytes.NewReader(testDataWithNewline)) // Create CLI instance cli, err := NewCLIInstance() if err != nil { t.Fatalf("failed to initialize CLI: %v", err) } cli.fs = fs cli.stateDir = testStateDir cli.cmd = cmd // Test adding the secret secretName := fmt.Sprintf("test-secret-%d", size) err = cli.AddSecret(secretName, false) if wantErr { require.Error(t, err) assert.Contains(t, err.Error(), errMsg) return } require.NoError(t, err) // Verify the secret was stored correctly retrievedValue, err := vlt.GetSecret(secretName) require.NoError(t, err) assert.Equal(t, testData, retrievedValue, "Retrieved secret should match original (without newline)") } // runImportSecretSizeCase imports a secret file of the given size and // verifies the outcome. func runImportSecretSizeCase(t *testing.T, size int, wantErr bool, errMsg string) { t.Helper() fs, vlt := newSizeTestVault(t) // Generate test data of specified size testData := make([]byte, size) _, err := rand.Read(testData) require.NoError(t, err) // Write test data to file testFile := fmt.Sprintf("/test/secret-%d.bin", size) err = afero.WriteFile(fs, testFile, testData, 0o600) require.NoError(t, err) // Create command cmd := &cobra.Command{} // Create CLI instance cli, err := NewCLIInstance() if err != nil { t.Fatalf("failed to initialize CLI: %v", err) } cli.fs = fs cli.stateDir = testStateDir // Test importing the secret secretName := fmt.Sprintf("imported-secret-%d", size) err = cli.ImportSecret(cmd, secretName, testFile, false) if wantErr { require.Error(t, err) assert.Contains(t, err.Error(), errMsg) return } require.NoError(t, err) // Verify the secret was stored correctly retrievedValue, err := vlt.GetSecret(secretName) require.NoError(t, err) assert.Equal(t, testData, retrievedValue, "Retrieved secret should match original") } // TestAddSecretVariousSizes tests adding secrets of various sizes through stdin // //nolint:paralleltest // subtests use t.Setenv via newSizeTestVault func TestAddSecretVariousSizes(t *testing.T) { tests := []struct { name string size int shouldError bool errorMsg string }{ { name: "1KB secret", size: 1024, shouldError: false, }, { name: "10KB secret", size: 10 * 1024, shouldError: false, }, { name: "100KB secret", size: 100 * 1024, shouldError: false, }, { name: "1MB secret", size: 1024 * 1024, shouldError: false, }, { name: "10MB secret", size: 10 * 1024 * 1024, shouldError: false, }, { name: "99MB secret", size: 99 * 1024 * 1024, shouldError: false, }, { name: "100MB secret minus 1 byte", size: 100*1024*1024 - 1, shouldError: false, }, { name: "101MB secret - should fail", size: 101 * 1024 * 1024, shouldError: true, errorMsg: "secret too large: exceeds 100MB limit", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { runAddSecretSizeCase(t, tt.size, tt.shouldError, tt.errorMsg) }) } } // TestImportSecretVariousSizes tests importing secrets of various sizes from files // //nolint:paralleltest // subtests use t.Setenv via newSizeTestVault func TestImportSecretVariousSizes(t *testing.T) { tests := []struct { name string size int shouldError bool errorMsg string }{ { name: "1KB file", size: 1024, shouldError: false, }, { name: "10KB file", size: 10 * 1024, shouldError: false, }, { name: "100KB file", size: 100 * 1024, shouldError: false, }, { name: "1MB file", size: 1024 * 1024, shouldError: false, }, { name: "10MB file", size: 10 * 1024 * 1024, shouldError: false, }, { name: "99MB file", size: 99 * 1024 * 1024, shouldError: false, }, { name: "100MB file", size: 100 * 1024 * 1024, shouldError: false, }, { name: "101MB file - should fail", size: 101 * 1024 * 1024, shouldError: true, errorMsg: "secret file too large: exceeds 100MB limit", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { runImportSecretSizeCase(t, tt.size, tt.shouldError, tt.errorMsg) }) } } // TestAddSecretBufferGrowth tests that our buffer growth strategy works correctly // //nolint:paralleltest // subtests use t.Setenv via newSizeTestVault func TestAddSecretBufferGrowth(t *testing.T) { // Test various sizes that should trigger buffer growth sizes := []int{ 1, // Single byte 100, // Small 4095, // Just under initial 4KB 4096, // Exactly 4KB 4097, // Just over 4KB 8191, // Just under 8KB (first double) 8192, // Exactly 8KB 8193, // Just over 8KB 12288, // 12KB (should trigger second double) 16384, // 16KB 32768, // 32KB (after more doublings) 65536, // 64KB 131072, // 128KB 524288, // 512KB 1048576, // 1MB 2097152, // 2MB } for _, size := range sizes { t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { fs, vlt := newSizeTestVault(t) // Create test data of exactly the specified size // Use a pattern that's easy to verify testData := make([]byte, size) for i := range testData { testData[i] = byte(i % 256) } // Create command with fake stdin (no newline) cmd := &cobra.Command{} cmd.SetIn(bytes.NewReader(testData)) // Create CLI instance cli, err := NewCLIInstance() if err != nil { t.Fatalf("failed to initialize CLI: %v", err) } cli.fs = fs cli.stateDir = testStateDir cli.cmd = cmd // Test adding the secret secretName := fmt.Sprintf("buffer-test-%d", size) err = cli.AddSecret(secretName, false) require.NoError(t, err) // Verify the secret was stored correctly retrievedValue, err := vlt.GetSecret(secretName) require.NoError(t, err) assert.Equal(t, testData, retrievedValue, "Retrieved secret should match original exactly") }) } } // TestAddSecretStreamingBehavior tests that we handle streaming input correctly // //nolint:paralleltest // uses t.Setenv via newSizeTestVault func TestAddSecretStreamingBehavior(t *testing.T) { fs, vlt := newSizeTestVault(t) // Create a custom reader that simulates slow streaming input // This will help verify our buffer handling works correctly with partial reads testData := []byte(strings.Repeat("Hello, World! ", 1000)) // ~14KB streamingStdin := &slowReader{ data: testData, chunkSize: 1000, // Read 1KB at a time } // Create command with slow reader as stdin cmd := &cobra.Command{} cmd.SetIn(streamingStdin) // Create CLI instance cli, err := NewCLIInstance() if err != nil { t.Fatalf("failed to initialize CLI: %v", err) } cli.fs = fs cli.stateDir = testStateDir cli.cmd = cmd // Test adding the secret err = cli.AddSecret("streaming-test", false) require.NoError(t, err) // Verify the secret was stored correctly retrievedValue, err := vlt.GetSecret("streaming-test") require.NoError(t, err) assert.Equal(t, testData, retrievedValue, "Retrieved secret should match original") } // slowReader simulates a reader that returns data in small chunks type slowReader struct { data []byte offset int chunkSize int } func (r *slowReader) Read(p []byte) (int, error) { if r.offset >= len(r.data) { return 0, io.EOF } // Read at most chunkSize bytes, bounded by the remaining data and // the destination buffer remaining := len(r.data) - r.offset toRead := min(r.chunkSize, remaining, len(p)) n := copy(p, r.data[r.offset:r.offset+toRead]) r.offset += n if r.offset >= len(r.data) { return n, io.EOF } return n, nil }