From ce6298f304ba722e5fdf9a20d261f5bce790dda9 Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Sun, 5 Apr 2026 15:16:48 +0300 Subject: [PATCH] test(04-02): add failing tests for DirSource recursive walk and mmap --- pkg/engine/sources/dir_test.go | 146 +++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 pkg/engine/sources/dir_test.go diff --git a/pkg/engine/sources/dir_test.go b/pkg/engine/sources/dir_test.go new file mode 100644 index 0000000..49759ec --- /dev/null +++ b/pkg/engine/sources/dir_test.go @@ -0,0 +1,146 @@ +package sources + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/salvacybersec/keyhunter/pkg/types" +) + +func drain(t *testing.T, src Source) []types.Chunk { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + out := make(chan types.Chunk, 1024) + errCh := make(chan error, 1) + go func() { errCh <- src.Chunks(ctx, out); close(out) }() + var got []types.Chunk + for c := range out { + got = append(got, c) + } + require.NoError(t, <-errCh) + return got +} + +func writeTestFile(t *testing.T, path, content string) { + t.Helper() + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(content), 0o644)) +} + +func isSortedStrings(s []string) bool { + for i := 1; i < len(s); i++ { + if s[i-1] > s[i] { + return false + } + } + return true +} + +func TestDirSource_RecursiveWalk(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "a.txt"), "alpha content") + writeTestFile(t, filepath.Join(root, "sub", "b.txt"), "bravo content") + writeTestFile(t, filepath.Join(root, "sub", "deep", "c.txt"), "charlie content") + + chunks := drain(t, NewDirSourceRaw(root, nil)) + require.Len(t, chunks, 3) + + sources := make([]string, 0, len(chunks)) + for _, c := range chunks { + sources = append(sources, c.Source) + } + require.True(t, isSortedStrings(sources), "emission order must be sorted, got %v", sources) +} + +func TestDirSource_DefaultExcludes(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "keep.txt"), "keep me") + writeTestFile(t, filepath.Join(root, ".git", "config"), "[core]") + writeTestFile(t, filepath.Join(root, "node_modules", "foo.js"), "x") + writeTestFile(t, filepath.Join(root, "vendor", "bar.go"), "package x") + writeTestFile(t, filepath.Join(root, "app.min.js"), "y") + writeTestFile(t, filepath.Join(root, "app.js.map"), "{}") + + chunks := drain(t, NewDirSource(root)) + require.Len(t, chunks, 1) + require.Contains(t, chunks[0].Source, "keep.txt") +} + +func TestDirSource_UserExclude(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "keep.txt"), "keep") + writeTestFile(t, filepath.Join(root, "drop.log"), "drop") + + chunks := drain(t, NewDirSourceRaw(root, []string{"*.log"})) + require.Len(t, chunks, 1) + require.Contains(t, chunks[0].Source, "keep.txt") +} + +func TestDirSource_BinarySkipped(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "text.txt"), "plain text content") + binPath := filepath.Join(root, "blob.bin") + require.NoError(t, os.WriteFile(binPath, []byte{0x7f, 'E', 'L', 'F', 0x00, 0x01, 0x02}, 0o644)) + + chunks := drain(t, NewDirSourceRaw(root, nil)) + require.Len(t, chunks, 1) + require.Contains(t, chunks[0].Source, "text.txt") +} + +func TestDirSource_MmapLargeFile(t *testing.T) { + if testing.Short() { + t.Skip("skipping large file test in short mode") + } + root := t.TempDir() + big := filepath.Join(root, "big.txt") + payload := strings.Repeat("API_KEY=xxxxxxxxxxxxxxxxxxxx\n", (int(MmapThreshold)/28)+10) + require.NoError(t, os.WriteFile(big, []byte(payload), 0o644)) + + chunks := drain(t, NewDirSourceRaw(root, nil)) + require.NotEmpty(t, chunks) + require.Equal(t, big, chunks[0].Source) +} + +func TestDirSource_DeterministicOrdering(t *testing.T) { + root := t.TempDir() + writeTestFile(t, filepath.Join(root, "zeta.txt"), "z") + writeTestFile(t, filepath.Join(root, "alpha.txt"), "a") + writeTestFile(t, filepath.Join(root, "mike.txt"), "m") + + run := func() []string { + chunks := drain(t, NewDirSourceRaw(root, nil)) + srcs := make([]string, 0, len(chunks)) + for _, c := range chunks { + srcs = append(srcs, c.Source) + } + return srcs + } + require.Equal(t, run(), run()) +} + +func TestDirSource_MissingRoot(t *testing.T) { + src := NewDirSourceRaw("/definitely/does/not/exist/keyhunter-xyz-zzz", nil) + ctx := context.Background() + out := make(chan types.Chunk, 1) + err := src.Chunks(ctx, out) + require.Error(t, err) +} + +func TestDirSource_CtxCancellation(t *testing.T) { + root := t.TempDir() + for i := 0; i < 50; i++ { + writeTestFile(t, filepath.Join(root, "f", string(rune('a'+i%26))+"_"+string(rune('a'+i%26))+".txt"), "payload") + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() // pre-cancelled + out := make(chan types.Chunk, 1024) + err := NewDirSourceRaw(root, nil).Chunks(ctx, out) + require.ErrorIs(t, err, context.Canceled) +}