Files
mfer/internal/cli/entry_test.go
sneak 3bfbb3fbe2
All checks were successful
check / check (push) Successful in 35s
Update golangci-lint to v2.12.2 with canonical config (closes #60)
- Add canonical .golangci.yml (v2 schema, default: all, project
  thresholds for lll/funlen/cyclop/dupl)
- Bump golangci-lint pins from v2.0.2 to v2.12.2 in Makefile
  (go install, new /v2 module path) and Dockerfile (tagged+digest
  Debian image pin)
- Fix all lint findings surfaced by the new linter set across
  cmd/mfer, internal/bork, internal/cli, internal/log, and mfer:
  static sentinel errors (err113), context-aware HTTP and exec
  (noctx), guarded integer conversions and stricter permissions
  (gosec), named constants (mnd, goconst), function decomposition
  (funlen, cyclop, gocognit, nestif), declaration ordering
  (funcorder), t.Parallel/t.TempDir/t.Setenv adoption in tests
  (paralleltest, usetesting), protobuf getters (protogetter), plus
  formatting and style cleanups (wsl_v5, nlreturn, lll, revive,
  testifylint, and others)
- Serialize CLI runs in tests behind a mutex so parallel tests do
  not cross-wire the process-global logger's captured output

The decompositions are behavior-preserving. In particular:

- REPO_POLICIES.md is untouched and stays byte-identical to the
  authoritative copy in the prompts repo
- the mfer.manifest type stays unexported; whether to export it is an
  open owner design question (README question 13)
- directories created by fetch keep mode 0755, because fetched trees
  are content meant to be readable by other uids
- an absent MFFilePath.Mtime is handled explicitly and identically in
  freshen, list, and export rather than being read as the Unix epoch,
  which would classify every entry as changed and rewrite the manifest
  on every freshen
- every user-visible error message renders byte-identically to what it
  did before, with the err113 sentinels wrapped mid-sentence where
  needed; the rendered strings are now pinned by tests

Also fixes an argument-injection defect the lint pass surfaced: key IDs
reach gpg as bare positional arguments, so a key ID beginning with "-"
was parsed by gpg as an option. All positional arguments now follow an
explicit "--" end-of-options marker.

The symlink-escape gap in fetch's path handling, which sanitizePath
does not and cannot address, is filed separately as #86.
2026-08-09 02:16:37 +00:00

765 lines
21 KiB
Go

//nolint:testpackage // white-box tests exercise unexported internals
package cli
import (
"bytes"
"errors"
"fmt"
"math/rand"
"os"
"sync"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
urfcli "github.com/urfave/cli/v2"
"sneak.berlin/go/mfer/mfer"
)
const (
testApp = "mfer"
testDir = "/testdir"
testFile1 = "/testdir/file1.txt"
testMF = "/testdir/test.mf"
testOutput = "/output.mf"
testOutputTmp = "/output.mf.tmp"
testManifest = "/manifest.mf"
testFlagBase = "--base"
testFlagNoExtra = "--no-extra-files"
)
var errSimulatedWrite = errors.New("simulated write failure")
// runMu serializes CLI runs: RunWithOptions wires the process-global
// logger to the run's I/O streams, so parallel runs would cross-wire
// captured output between tests.
//
//nolint:gochecknoglobals // guards process-global logger state in tests
var runMu sync.Mutex
// runCLI invokes RunWithOptions while holding runMu so parallel tests
// capture their own output.
func runCLI(opts *RunOptions) int {
runMu.Lock()
defer runMu.Unlock()
return RunWithOptions(opts)
}
func TestMain(m *testing.M) {
// Prevent urfave/cli from calling os.Exit during tests
urfcli.OsExiter = func(_ int) {}
os.Exit(m.Run())
}
func TestBuild(t *testing.T) {
t.Parallel()
m := &CLIApp{}
assert.NotNil(t, m)
}
func testOpts(args []string, fs afero.Fs) *RunOptions {
return &RunOptions{
Appname: testApp,
Version: "1.0.0",
Gitrev: "abc123",
Args: args,
Stdin: &bytes.Buffer{},
Stdout: &bytes.Buffer{},
Stderr: &bytes.Buffer{},
Fs: fs,
}
}
func testStdout(t *testing.T, opts *RunOptions) string {
t.Helper()
buf, ok := opts.Stdout.(*bytes.Buffer)
require.True(t, ok)
return buf.String()
}
func testStderr(t *testing.T, opts *RunOptions) string {
t.Helper()
buf, ok := opts.Stderr.(*bytes.Buffer)
require.True(t, ok)
return buf.String()
}
func writeTestFile(t *testing.T, fs afero.Fs, path, content string) {
t.Helper()
require.NoError(t, afero.WriteFile(fs, path, []byte(content), 0o644))
}
func TestVersionCommand(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
opts := testOpts([]string{testApp, "version"}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 0, exitCode)
stdout := testStdout(t, opts)
assert.Contains(t, stdout, mfer.Version)
assert.Contains(t, stdout, "abc123")
}
func TestHelpCommand(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
opts := testOpts([]string{testApp, "--help"}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 0, exitCode)
stdout := testStdout(t, opts)
assert.Contains(t, stdout, cmdGenerate)
assert.Contains(t, stdout, cmdCheck)
assert.Contains(t, stdout, "fetch")
}
func TestGenerateCommand(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test files in memory filesystem
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello world")
writeTestFile(t, fs, "/testdir/file2.txt", "test content")
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
// Verify manifest was created
exists, err := afero.Exists(fs, testMF)
require.NoError(t, err)
assert.True(t, exists)
}
func TestGenerateAndCheckCommand(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test files with subdirectory
require.NoError(t, fs.MkdirAll("/testdir/subdir", 0o755))
writeTestFile(t, fs, testFile1, "hello world")
writeTestFile(t, fs, "/testdir/subdir/file2.txt", "test content")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode, "generate failed: %s", testStderr(t, opts))
// Check manifest
opts = testOpts([]string{testApp, cmdCheck, "-q", testFlagBase, testDir, testMF}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 0, exitCode, "check failed: %s", testStderr(t, opts))
}
func TestCheckCommandWithMissingFile(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test file
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello world")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode, "generate failed: %s", testStderr(t, opts))
// Delete the file
require.NoError(t, fs.Remove(testFile1))
// Check manifest - should fail
opts = testOpts([]string{testApp, cmdCheck, "-q", testFlagBase, testDir, testMF}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 1, exitCode, "check should have failed for missing file")
}
func runCheckAfterRewrite(t *testing.T, rewritten, msg string) {
t.Helper()
fs := afero.NewMemMapFs()
// Create test file
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello world")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode, "generate failed: %s", testStderr(t, opts))
// Rewrite the file, then check the manifest - it must fail
writeTestFile(t, fs, testFile1, rewritten)
opts = testOpts([]string{testApp, cmdCheck, "-q", testFlagBase, testDir, testMF}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 1, exitCode, msg)
}
func TestCheckCommandWithCorruptedFile(t *testing.T) {
t.Parallel()
// Corrupt the file (change content but keep same size)
runCheckAfterRewrite(t, "HELLO WORLD",
"check should have failed for corrupted file")
}
func TestCheckCommandWithSizeMismatch(t *testing.T) {
t.Parallel()
// Change file size
runCheckAfterRewrite(t, "different size content here",
"check should have failed for size mismatch")
}
func TestBannerOutput(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test file
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
// Run without -q to see banner
opts := testOpts([]string{testApp, cmdGenerate, "-o", testMF, testDir}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 0, exitCode)
// Banner ASCII art should be in stdout
stdout := testStdout(t, opts)
assert.Contains(t, stdout, "___")
assert.Contains(t, stdout, "\\")
}
func TestUnknownCommand(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
opts := testOpts([]string{testApp, "unknown"}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 1, exitCode)
}
func TestGenerateExcludesDotfilesByDefault(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test files including dotfiles
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
writeTestFile(t, fs, "/testdir/.hidden", "secret")
// Generate manifest without --include-dotfiles (default excludes dotfiles)
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Check that manifest exists
exists, _ := afero.Exists(fs, testMF)
assert.True(t, exists)
// Verify manifest only has 1 file (the non-dotfile)
manifest, err := mfer.NewManifestFromFile(fs, testMF)
require.NoError(t, err)
assert.Len(t, manifest.Files(), 1)
assert.Equal(t, "file1.txt", manifest.Files()[0].GetPath())
}
func TestGenerateWithIncludeDotfiles(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test files including dotfiles
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
writeTestFile(t, fs, "/testdir/.hidden", "secret")
// Generate manifest with --include-dotfiles
opts := testOpts([]string{
testApp, cmdGenerate, "-q", "--include-dotfiles", "-o", testMF, testDir,
}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Verify manifest has 2 files (including dotfile)
manifest, err := mfer.NewManifestFromFile(fs, testMF)
require.NoError(t, err)
assert.Len(t, manifest.Files(), 2)
}
func TestMultipleInputPaths(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test files in multiple directories
require.NoError(t, fs.MkdirAll("/dir1", 0o755))
require.NoError(t, fs.MkdirAll("/dir2", 0o755))
writeTestFile(t, fs, "/dir1/file1.txt", "content1")
writeTestFile(t, fs, "/dir2/file2.txt", "content2")
// Generate manifest from multiple paths
opts := testOpts([]string{
testApp, cmdGenerate, "-q", "-o", testOutput, "/dir1", "/dir2",
}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
exists, _ := afero.Exists(fs, testOutput)
assert.True(t, exists)
}
func TestNoExtraFilesPass(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test files
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
writeTestFile(t, fs, "/testdir/file2.txt", "world")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Check with --no-extra-files (should pass - no extra files)
opts = testOpts([]string{
testApp, cmdCheck, "-q", testFlagNoExtra, testFlagBase, testDir, testManifest,
}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 0, exitCode)
}
func TestNoExtraFilesFail(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test files
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Add an extra file after manifest generation
writeTestFile(t, fs, "/testdir/extra.txt", "extra")
// Check with --no-extra-files (should fail - extra file exists)
opts = testOpts([]string{
testApp, cmdCheck, "-q", testFlagNoExtra, testFlagBase, testDir, testManifest,
}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 1, exitCode, "check should fail when extra files exist")
}
func TestNoExtraFilesWithSubdirectory(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test files with subdirectory
require.NoError(t, fs.MkdirAll("/testdir/subdir", 0o755))
writeTestFile(t, fs, testFile1, "hello")
writeTestFile(t, fs, "/testdir/subdir/file2.txt", "world")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Add extra file in subdirectory
writeTestFile(t, fs, "/testdir/subdir/extra.txt", "extra")
// Check with --no-extra-files (should fail)
opts = testOpts([]string{
testApp, cmdCheck, "-q", testFlagNoExtra, testFlagBase, testDir, testManifest,
}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 1, exitCode,
"check should fail when extra files exist in subdirectory")
}
func TestCheckWithoutNoExtraFilesIgnoresExtra(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test file
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Add extra file
writeTestFile(t, fs, "/testdir/extra.txt", "extra")
// Check WITHOUT --no-extra-files (should pass - extra files ignored)
opts = testOpts([]string{
testApp, cmdCheck, "-q", testFlagBase, testDir, testManifest,
}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 0, exitCode,
"check without --no-extra-files should ignore extra files")
}
func TestGenerateAtomicWriteNoTempFileOnSuccess(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test file
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testOutput, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Verify output file exists
exists, err := afero.Exists(fs, testOutput)
require.NoError(t, err)
assert.True(t, exists, "output file should exist")
// Verify temp file does NOT exist
tmpExists, err := afero.Exists(fs, testOutputTmp)
require.NoError(t, err)
assert.False(t, tmpExists,
"temp file should not exist after successful generation")
}
func TestGenerateAtomicWriteOverwriteWithForce(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test file
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
// Create existing manifest with different content
writeTestFile(t, fs, testOutput, "old content")
// Generate manifest with --force
opts := testOpts([]string{
testApp, cmdGenerate, "-q", "-f", "-o", testOutput, testDir,
}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Verify output file exists and was overwritten
content, err := afero.ReadFile(fs, testOutput)
require.NoError(t, err)
assert.NotEqual(t, "old content", string(content),
"manifest should be overwritten")
// Verify temp file does NOT exist
tmpExists, err := afero.Exists(fs, testOutputTmp)
require.NoError(t, err)
assert.False(t, tmpExists,
"temp file should not exist after successful generation")
}
func TestGenerateFailsWithoutForceWhenOutputExists(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
// Create test file
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
// Create existing manifest
writeTestFile(t, fs, testOutput, "existing")
// Generate manifest WITHOUT --force (should fail)
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testOutput, testDir}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 1, exitCode, "should fail when output exists without --force")
// Verify original content is preserved
content, err := afero.ReadFile(fs, testOutput)
require.NoError(t, err)
assert.Equal(t, "existing", string(content), "original file should be preserved")
}
func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
t.Parallel()
// This test verifies that generate uses a temp file by checking
// that the output file doesn't exist until generation completes.
// We do this by generating to a path and verifying the temp file
// pattern is used (output.mf.tmp -> output.mf)
fs := afero.NewMemMapFs()
// Create test file
require.NoError(t, fs.MkdirAll(testDir, 0o755))
writeTestFile(t, fs, testFile1, "hello")
// Generate manifest
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testOutput, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode)
// Both output file should exist and temp should not
exists, _ := afero.Exists(fs, testOutput)
assert.True(t, exists, "output file should exist")
tmpExists, _ := afero.Exists(fs, testOutputTmp)
assert.False(t, tmpExists, "temp file should be cleaned up")
// Verify manifest is valid (not empty)
content, err := afero.ReadFile(fs, testOutput)
require.NoError(t, err)
assert.NotEmpty(t, content, "manifest should not be empty")
}
// failingWriterFs wraps a filesystem and makes writes fail after N bytes
type failingWriterFs struct {
afero.Fs
failAfter int64
written int64
}
type failingFile struct {
afero.File
fs *failingWriterFs
}
func (f *failingFile) Write(p []byte) (int, error) {
f.fs.written += int64(len(p))
if f.fs.written > f.fs.failAfter {
return 0, errSimulatedWrite
}
return f.File.Write(p)
}
//nolint:ireturn // Create must return afero.File to satisfy afero.Fs.
func (fs *failingWriterFs) Create(name string) (afero.File, error) {
f, err := fs.Fs.Create(name)
if err != nil {
return nil, err
}
return &failingFile{File: f, fs: fs}, nil
}
func TestGenerateAtomicWriteCleansUpOnError(t *testing.T) {
t.Parallel()
baseFs := afero.NewMemMapFs()
// Create test files - need enough content to trigger the write failure
require.NoError(t, baseFs.MkdirAll(testDir, 0o755))
writeTestFile(t, baseFs, testFile1, "hello world this is a test file")
// Wrap with failing writer that fails after writing some bytes
fs := &failingWriterFs{Fs: baseFs, failAfter: 10}
// Generate manifest - should fail during write
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testOutput, testDir}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 1, exitCode, "should fail due to write error")
// With atomic writes: output.mf should NOT exist (temp was cleaned up)
// With non-atomic writes: output.mf WOULD exist (partial/empty)
exists, _ := afero.Exists(baseFs, testOutput)
assert.False(t, exists,
"output file should not exist after failed generation (atomic write)")
// Temp file should also not exist
tmpExists, _ := afero.Exists(baseFs, testOutputTmp)
assert.False(t, tmpExists,
"temp file should be cleaned up after failed generation")
}
func TestGenerateValidatesInputPaths(t *testing.T) {
t.Parallel()
seedValidDir := func(t *testing.T, fs afero.Fs) {
t.Helper()
require.NoError(t, fs.MkdirAll("/validdir", 0o755))
writeTestFile(t, fs, "/validdir/file.txt", "content")
}
t.Run("nonexistent path fails fast", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
seedValidDir(t, fs)
opts := testOpts([]string{
testApp, cmdGenerate, "-q", "-o", testOutput, "/nonexistent",
}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 1, exitCode)
stderr := testStderr(t, opts)
assert.Contains(t, stderr, "path does not exist")
assert.Contains(t, stderr, "/nonexistent")
})
t.Run("mix of valid and invalid paths fails fast", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
seedValidDir(t, fs)
opts := testOpts([]string{
testApp, cmdGenerate, "-q", "-o", testOutput,
"/validdir", "/alsononexistent",
}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 1, exitCode)
stderr := testStderr(t, opts)
assert.Contains(t, stderr, "path does not exist")
assert.Contains(t, stderr, "/alsononexistent")
// Output file should not have been created
exists, _ := afero.Exists(fs, testOutput)
assert.False(t, exists,
"output file should not exist when path validation fails")
})
t.Run("valid paths succeed", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
seedValidDir(t, fs)
opts := testOpts([]string{
testApp, cmdGenerate, "-q", "-o", testOutput, "/validdir",
}, fs)
exitCode := runCLI(opts)
assert.Equal(t, 0, exitCode)
})
}
func TestCheckDetectsManifestCorruption(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
rng := rand.New(rand.NewSource(42)) //nolint:gosec // deterministic test data
// Create many small files with random names to generate a ~1MB manifest
// Each manifest entry is roughly 50-60 bytes, so we need ~20000 files
require.NoError(t, fs.MkdirAll(testDir, 0o755))
numFiles := 20000
for range numFiles {
// Generate random filename
filename := fmt.Sprintf("/testdir/%08x%08x%08x.dat",
rng.Uint32(), rng.Uint32(), rng.Uint32())
// Small random content
content := make([]byte, 16+rng.Intn(48))
_, _ = rng.Read(content)
require.NoError(t, afero.WriteFile(fs, filename, content, 0o644))
}
// Generate manifest outside of testdir
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testManifest, testDir}, fs)
exitCode := runCLI(opts)
require.Equal(t, 0, exitCode, "generate should succeed")
// Read the valid manifest and verify it's approximately 1MB
validManifest, err := afero.ReadFile(fs, testManifest)
require.NoError(t, err)
require.GreaterOrEqual(t, len(validManifest), 1024*1024,
"manifest should be at least 1MB, got %d bytes", len(validManifest))
t.Logf("manifest size: %d bytes (%d files)", len(validManifest), numFiles)
// First corruption: truncate the manifest
require.NoError(t, afero.WriteFile(fs, testManifest,
validManifest[:len(validManifest)/2], 0o644))
// Check should fail with truncated manifest
opts = testOpts([]string{
testApp, cmdCheck, "-q", testFlagBase, testDir, testManifest,
}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 1, exitCode, "check should fail with truncated manifest")
// Verify check passes with valid manifest
require.NoError(t, afero.WriteFile(fs, testManifest, validManifest, 0o644))
opts = testOpts([]string{
testApp, cmdCheck, "-q", testFlagBase, testDir, testManifest,
}, fs)
exitCode = runCLI(opts)
require.Equal(t, 0, exitCode, "check should pass with valid manifest")
// Now do 500 random corruption iterations
for i := range 500 {
// Corrupt: write a random byte at a random offset
corrupted := make([]byte, len(validManifest))
copy(corrupted, validManifest)
offset := rng.Intn(len(corrupted))
originalByte := corrupted[offset]
// Make sure we actually change the byte
buf := make([]byte, 1)
newByte := originalByte
for newByte == originalByte {
_, _ = rng.Read(buf)
newByte = buf[0]
}
corrupted[offset] = newByte
require.NoError(t, afero.WriteFile(fs, testManifest, corrupted, 0o644))
// Check should fail with corrupted manifest
opts = testOpts([]string{
testApp, cmdCheck, "-q", testFlagBase, testDir, testManifest,
}, fs)
exitCode = runCLI(opts)
assert.Equal(t, 1, exitCode,
"iteration %d: check should fail with corrupted manifest "+
"(offset %d, 0x%02x -> 0x%02x)",
i, offset, originalByte, newByte)
// Restore valid manifest for next iteration
require.NoError(t, afero.WriteFile(fs, testManifest, validManifest, 0o644))
}
}