package cli import ( "bytes" "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" ) func init() { // Prevent urfave/cli from calling os.Exit during tests urfcli.OsExiter = func(code int) {} } func TestBuild(t *testing.T) { m := &CLIApp{} assert.NotNil(t, m) } func testOpts(args []string, fs afero.Fs) *RunOptions { return &RunOptions{ Appname: "mfer", Version: "1.0.0", Gitrev: "abc123", Args: args, Stdin: &bytes.Buffer{}, Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}, Fs: fs, } } func TestVersionCommand(t *testing.T) { fs := afero.NewMemMapFs() opts := testOpts([]string{"mfer", "version"}, fs) exitCode := RunWithOptions(opts) assert.Equal(t, 0, exitCode) stdout := opts.Stdout.(*bytes.Buffer).String() assert.Contains(t, stdout, "1.0.0") assert.Contains(t, stdout, "abc123") } func TestHelpCommand(t *testing.T) { fs := afero.NewMemMapFs() opts := testOpts([]string{"mfer", "--help"}, fs) exitCode := RunWithOptions(opts) assert.Equal(t, 0, exitCode) stdout := opts.Stdout.(*bytes.Buffer).String() assert.Contains(t, stdout, "generate") assert.Contains(t, stdout, "check") assert.Contains(t, stdout, "fetch") } func TestGenerateCommand(t *testing.T) { fs := afero.NewMemMapFs() // Create test files in memory filesystem require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644)) require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("test content"), 0644)) opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/testdir/test.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) assert.Equal(t, 0, exitCode, "stderr: %s", opts.Stderr.(*bytes.Buffer).String()) // Verify manifest was created exists, err := afero.Exists(fs, "/testdir/test.mf") require.NoError(t, err) assert.True(t, exists) } func TestGenerateAndCheckCommand(t *testing.T) { fs := afero.NewMemMapFs() // Create test files with subdirectory require.NoError(t, fs.MkdirAll("/testdir/subdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644)) require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/file2.txt", []byte("test content"), 0644)) // Generate manifest opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/testdir/test.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode, "generate failed: %s", opts.Stderr.(*bytes.Buffer).String()) // Check manifest opts = testOpts([]string{"mfer", "-q", "check", "--base", "/testdir", "/testdir/test.mf"}, fs) exitCode = RunWithOptions(opts) assert.Equal(t, 0, exitCode, "check failed: %s", opts.Stderr.(*bytes.Buffer).String()) } func TestCheckCommandWithMissingFile(t *testing.T) { fs := afero.NewMemMapFs() // Create test file require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644)) // Generate manifest opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/testdir/test.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode, "generate failed: %s", opts.Stderr.(*bytes.Buffer).String()) // Delete the file require.NoError(t, fs.Remove("/testdir/file1.txt")) // Check manifest - should fail opts = testOpts([]string{"mfer", "-q", "check", "--base", "/testdir", "/testdir/test.mf"}, fs) exitCode = RunWithOptions(opts) assert.Equal(t, 1, exitCode, "check should have failed for missing file") } func TestCheckCommandWithCorruptedFile(t *testing.T) { fs := afero.NewMemMapFs() // Create test file require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644)) // Generate manifest opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/testdir/test.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode, "generate failed: %s", opts.Stderr.(*bytes.Buffer).String()) // Corrupt the file (change content but keep same size) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("HELLO WORLD"), 0644)) // Check manifest - should fail with hash mismatch opts = testOpts([]string{"mfer", "-q", "check", "--base", "/testdir", "/testdir/test.mf"}, fs) exitCode = RunWithOptions(opts) assert.Equal(t, 1, exitCode, "check should have failed for corrupted file") } func TestCheckCommandWithSizeMismatch(t *testing.T) { fs := afero.NewMemMapFs() // Create test file require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello world"), 0644)) // Generate manifest opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/testdir/test.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode, "generate failed: %s", opts.Stderr.(*bytes.Buffer).String()) // Change file size require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("different size content here"), 0644)) // Check manifest - should fail with size mismatch opts = testOpts([]string{"mfer", "-q", "check", "--base", "/testdir", "/testdir/test.mf"}, fs) exitCode = RunWithOptions(opts) assert.Equal(t, 1, exitCode, "check should have failed for size mismatch") } func TestBannerOutput(t *testing.T) { fs := afero.NewMemMapFs() // Create test file require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644)) // Run without -q to see banner opts := testOpts([]string{"mfer", "generate", "-o", "/testdir/test.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) assert.Equal(t, 0, exitCode) // Banner ASCII art should be in stdout stdout := opts.Stdout.(*bytes.Buffer).String() assert.Contains(t, stdout, "___") assert.Contains(t, stdout, "\\") } func TestUnknownCommand(t *testing.T) { fs := afero.NewMemMapFs() opts := testOpts([]string{"mfer", "unknown"}, fs) exitCode := RunWithOptions(opts) assert.Equal(t, 1, exitCode) } func TestGenerateExcludesDotfilesByDefault(t *testing.T) { fs := afero.NewMemMapFs() // Create test files including dotfiles require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644)) require.NoError(t, afero.WriteFile(fs, "/testdir/.hidden", []byte("secret"), 0644)) // Generate manifest without --include-dotfiles (default excludes dotfiles) opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/testdir/test.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode) // Check that manifest exists exists, _ := afero.Exists(fs, "/testdir/test.mf") assert.True(t, exists) // Verify manifest only has 1 file (the non-dotfile) manifest, err := mfer.NewManifestFromFile(fs, "/testdir/test.mf") require.NoError(t, err) assert.Len(t, manifest.Files(), 1) assert.Equal(t, "file1.txt", manifest.Files()[0].Path) } func TestGenerateWithIncludeDotfiles(t *testing.T) { fs := afero.NewMemMapFs() // Create test files including dotfiles require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644)) require.NoError(t, afero.WriteFile(fs, "/testdir/.hidden", []byte("secret"), 0644)) // Generate manifest with --include-dotfiles opts := testOpts([]string{"mfer", "-q", "generate", "--include-dotfiles", "-o", "/testdir/test.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode) // Verify manifest has 2 files (including dotfile) manifest, err := mfer.NewManifestFromFile(fs, "/testdir/test.mf") require.NoError(t, err) assert.Len(t, manifest.Files(), 2) } func TestMultipleInputPaths(t *testing.T) { fs := afero.NewMemMapFs() // Create test files in multiple directories require.NoError(t, fs.MkdirAll("/dir1", 0755)) require.NoError(t, fs.MkdirAll("/dir2", 0755)) require.NoError(t, afero.WriteFile(fs, "/dir1/file1.txt", []byte("content1"), 0644)) require.NoError(t, afero.WriteFile(fs, "/dir2/file2.txt", []byte("content2"), 0644)) // Generate manifest from multiple paths opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/output.mf", "/dir1", "/dir2"}, fs) exitCode := RunWithOptions(opts) assert.Equal(t, 0, exitCode, "stderr: %s", opts.Stderr.(*bytes.Buffer).String()) exists, _ := afero.Exists(fs, "/output.mf") assert.True(t, exists) } func TestNoExtraFilesPass(t *testing.T) { fs := afero.NewMemMapFs() // Create test files require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644)) require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("world"), 0644)) // Generate manifest opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/manifest.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode) // Check with --no-extra-files (should pass - no extra files) opts = testOpts([]string{"mfer", "-q", "check", "--no-extra-files", "--base", "/testdir", "/manifest.mf"}, fs) exitCode = RunWithOptions(opts) assert.Equal(t, 0, exitCode) } func TestNoExtraFilesFail(t *testing.T) { fs := afero.NewMemMapFs() // Create test files require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644)) // Generate manifest opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/manifest.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode) // Add an extra file after manifest generation require.NoError(t, afero.WriteFile(fs, "/testdir/extra.txt", []byte("extra"), 0644)) // Check with --no-extra-files (should fail - extra file exists) opts = testOpts([]string{"mfer", "-q", "check", "--no-extra-files", "--base", "/testdir", "/manifest.mf"}, fs) exitCode = RunWithOptions(opts) assert.Equal(t, 1, exitCode, "check should fail when extra files exist") } func TestNoExtraFilesWithSubdirectory(t *testing.T) { fs := afero.NewMemMapFs() // Create test files with subdirectory require.NoError(t, fs.MkdirAll("/testdir/subdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644)) require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/file2.txt", []byte("world"), 0644)) // Generate manifest opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/manifest.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode) // Add extra file in subdirectory require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/extra.txt", []byte("extra"), 0644)) // Check with --no-extra-files (should fail) opts = testOpts([]string{"mfer", "-q", "check", "--no-extra-files", "--base", "/testdir", "/manifest.mf"}, fs) exitCode = RunWithOptions(opts) assert.Equal(t, 1, exitCode, "check should fail when extra files exist in subdirectory") } func TestCheckWithoutNoExtraFilesIgnoresExtra(t *testing.T) { fs := afero.NewMemMapFs() // Create test file require.NoError(t, fs.MkdirAll("/testdir", 0755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("hello"), 0644)) // Generate manifest opts := testOpts([]string{"mfer", "-q", "generate", "-o", "/manifest.mf", "/testdir"}, fs) exitCode := RunWithOptions(opts) require.Equal(t, 0, exitCode) // Add extra file require.NoError(t, afero.WriteFile(fs, "/testdir/extra.txt", []byte("extra"), 0644)) // Check WITHOUT --no-extra-files (should pass - extra files ignored) opts = testOpts([]string{"mfer", "-q", "check", "--base", "/testdir", "/manifest.mf"}, fs) exitCode = RunWithOptions(opts) assert.Equal(t, 0, exitCode, "check without --no-extra-files should ignore extra files") }