Cybersecurity-Projects/PROJECTS/intermediate/secrets-scanner/internal/source/source_test.go

430 lines
10 KiB
Go

/*
©AngelaMos | 2026
source_test.go
Tests for source/directory.go and source/git.go
Tests:
Directory source emits chunks for text files, skips binary extensions
Directory exclude patterns suppress matching files
Directory skips .git directory automatically
Max file size limit skips oversized files
Files longer than 50 lines split into multiple chunks with correct LineStart
Context cancellation propagates from Chunks() as context.Canceled
Directory String() returns "directory:<path>"
Git source emits chunks with CommitSHA and Author metadata from commit history
Git staged-only mode scans the index rather than commit history
Git depth limit restricts the number of commits scanned
Git exclude patterns skip matching files from history
Git String() returns "git:<path>"
splitIntoChunks produces 50-line windows with correct LineStart offsets
isBinaryExt correctly classifies binary and text extension types
*/
package source
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/CarterPerez-dev/portia/pkg/types"
)
func collectChunks(
t *testing.T, src Source,
) []types.Chunk {
t.Helper()
out := make(chan types.Chunk, 100)
ctx := context.Background()
errCh := make(chan error, 1)
go func() {
errCh <- src.Chunks(ctx, out)
close(out)
}()
var chunks []types.Chunk
for c := range out {
chunks = append(chunks, c)
}
require.NoError(t, <-errCh)
return chunks
}
func TestDirectoryChunks(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "config.py"),
[]byte(`password = "sk_live_4eC39HqLyjWDarjtT1zdp7dc"`+"\n"),
0o644,
))
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "logo.png"),
[]byte{0x89, 0x50, 0x4E, 0x47},
0o644,
))
src := NewDirectory(dir, 0, nil)
chunks := collectChunks(t, src)
require.Len(t, chunks, 1)
assert.Equal(t, "config.py", chunks[0].FilePath)
assert.Contains(t, chunks[0].Content, "sk_live_")
assert.Equal(t, 1, chunks[0].LineStart)
}
func TestDirectoryExcludes(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "main.go"),
[]byte("package main\n"),
0o644,
))
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "secret.env"),
[]byte("API_KEY=test\n"),
0o644,
))
src := NewDirectory(dir, 0, []string{"*.env"})
chunks := collectChunks(t, src)
require.Len(t, chunks, 1)
assert.Equal(t, "main.go", chunks[0].FilePath)
}
func TestDirectorySkipsDotGit(t *testing.T) {
t.Parallel()
dir := t.TempDir()
gitDir := filepath.Join(dir, ".git", "objects")
require.NoError(t, os.MkdirAll(gitDir, 0o755)) //nolint:gosec
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(gitDir, "data"),
[]byte("git internal data\n"),
0o644,
))
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "app.go"),
[]byte("package main\n"),
0o644,
))
src := NewDirectory(dir, 0, nil)
chunks := collectChunks(t, src)
require.Len(t, chunks, 1)
assert.Equal(t, "app.go", chunks[0].FilePath)
}
func TestDirectoryMaxSize(t *testing.T) {
t.Parallel()
dir := t.TempDir()
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "small.txt"),
[]byte("small\n"),
0o644,
))
bigContent := make([]byte, 2*1024*1024)
for i := range bigContent {
bigContent[i] = 'x'
}
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "big.txt"),
bigContent,
0o644,
))
src := NewDirectory(dir, 1024*1024, nil)
chunks := collectChunks(t, src)
require.Len(t, chunks, 1)
assert.Equal(t, "small.txt", chunks[0].FilePath)
}
func TestDirectoryChunking(t *testing.T) {
t.Parallel()
dir := t.TempDir()
var lines string
for i := range 120 {
lines += "line " + string(rune('A'+i%26)) + "\n"
}
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "long.txt"),
[]byte(lines),
0o644,
))
src := NewDirectory(dir, 0, nil)
chunks := collectChunks(t, src)
require.GreaterOrEqual(t, len(chunks), 2)
assert.Equal(t, 1, chunks[0].LineStart)
assert.Equal(t, 51, chunks[1].LineStart)
}
func TestDirectoryContextCancel(t *testing.T) {
t.Parallel()
dir := t.TempDir()
for i := range 10 {
name := filepath.Join(dir, "file"+string(rune('0'+i))+".txt")
require.NoError(t, os.WriteFile( //nolint:gosec
name, []byte("content\n"), 0o644,
))
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
src := NewDirectory(dir, 0, nil)
out := make(chan types.Chunk, 100)
err := src.Chunks(ctx, out)
close(out)
assert.ErrorIs(t, err, context.Canceled)
}
func TestDirectoryString(t *testing.T) {
t.Parallel()
src := NewDirectory("/tmp/test", 0, nil)
assert.Equal(t, "directory:/tmp/test", src.String())
}
func TestGitChunks(t *testing.T) {
t.Parallel()
dir := t.TempDir()
repo, err := git.PlainInit(dir, false)
require.NoError(t, err)
wt, err := repo.Worktree()
require.NoError(t, err)
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "secret.py"),
[]byte(`api_key = "sk_live_4eC39HqLyjWDarjtT1zdp7dc"`+"\n"),
0o644,
))
_, err = wt.Add("secret.py")
require.NoError(t, err)
_, err = wt.Commit("add secret", &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
When: time.Now(),
},
})
require.NoError(t, err)
src := NewGit(dir, "", "", 0, false, 0, nil)
chunks := collectChunks(t, src)
require.GreaterOrEqual(t, len(chunks), 1)
assert.Equal(t, "secret.py", chunks[0].FilePath)
assert.Contains(t, chunks[0].Content, "sk_live_")
assert.NotEmpty(t, chunks[0].CommitSHA)
assert.Equal(t, "test@example.com", chunks[0].Author)
}
func TestGitStagedOnly(t *testing.T) {
t.Parallel()
dir := t.TempDir()
repo, err := git.PlainInit(dir, false)
require.NoError(t, err)
wt, err := repo.Worktree()
require.NoError(t, err)
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "committed.txt"),
[]byte("committed content\n"),
0o644,
))
_, err = wt.Add("committed.txt")
require.NoError(t, err)
_, err = wt.Commit("initial", &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
When: time.Now(),
},
})
require.NoError(t, err)
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "staged.txt"),
[]byte("new staged content\n"),
0o644,
))
_, err = wt.Add("staged.txt")
require.NoError(t, err)
src := NewGit(dir, "", "", 0, true, 0, nil)
chunks := collectChunks(t, src)
found := false
for _, c := range chunks {
if c.FilePath == "staged.txt" {
found = true
assert.Contains(t, c.Content, "new staged content")
}
}
assert.True(t, found, "expected to find staged.txt chunk")
}
func TestGitDepthLimit(t *testing.T) {
t.Parallel()
dir := t.TempDir()
repo, err := git.PlainInit(dir, false)
require.NoError(t, err)
wt, err := repo.Worktree()
require.NoError(t, err)
for i := range 5 {
name := "file" + string(rune('0'+i)) + ".txt"
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, name),
[]byte("content "+name+"\n"),
0o644,
))
_, err = wt.Add(name)
require.NoError(t, err)
_, err = wt.Commit("commit "+name, &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
When: time.Now().Add(time.Duration(i) * time.Minute),
},
})
require.NoError(t, err)
}
src := NewGit(dir, "", "", 2, false, 0, nil)
chunks := collectChunks(t, src)
files := make(map[string]bool)
for _, c := range chunks {
files[c.FilePath] = true
}
assert.LessOrEqual(t, len(files), 5)
}
func TestGitExcludes(t *testing.T) {
t.Parallel()
dir := t.TempDir()
repo, err := git.PlainInit(dir, false)
require.NoError(t, err)
wt, err := repo.Worktree()
require.NoError(t, err)
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "keep.go"),
[]byte("package main\n"),
0o644,
))
require.NoError(t, os.WriteFile( //nolint:gosec
filepath.Join(dir, "skip.lock"),
[]byte("lock data\n"),
0o644,
))
_, err = wt.Add("keep.go")
require.NoError(t, err)
_, err = wt.Add("skip.lock")
require.NoError(t, err)
_, err = wt.Commit("add files", &git.CommitOptions{
Author: &object.Signature{
Name: "Test",
Email: "test@example.com",
When: time.Now(),
},
})
require.NoError(t, err)
src := NewGit(dir, "", "", 0, false, 0, []string{".lock"})
chunks := collectChunks(t, src)
for _, c := range chunks {
assert.NotContains(t, c.FilePath, ".lock")
}
}
func TestGitString(t *testing.T) {
t.Parallel()
src := NewGit("/tmp/repo", "", "", 0, false, 0, nil)
assert.Equal(t, "git:/tmp/repo", src.String())
}
func TestSplitIntoChunks(t *testing.T) {
t.Parallel()
var lines []string
for i := range 120 {
lines = append(lines, "line "+string(rune('A'+i%26)))
}
content := ""
for i, l := range lines {
if i > 0 {
content += "\n"
}
content += l
}
chunks := splitIntoChunks(
content, "test.txt", "abc123", "dev@test.com", time.Now(),
)
require.Len(t, chunks, 3)
assert.Equal(t, 1, chunks[0].LineStart)
assert.Equal(t, 51, chunks[1].LineStart)
assert.Equal(t, 101, chunks[2].LineStart)
assert.Equal(t, "abc123", chunks[0].CommitSHA)
assert.Equal(t, "dev@test.com", chunks[0].Author)
}
func TestSplitIntoChunksEmpty(t *testing.T) {
t.Parallel()
chunks := splitIntoChunks(
"", "test.txt", "", "", time.Time{},
)
assert.Empty(t, chunks)
}
func TestIsBinaryExt(t *testing.T) {
t.Parallel()
assert.True(t, isBinaryExt("image.png"))
assert.True(t, isBinaryExt("data.PDF"))
assert.True(t, isBinaryExt("app.exe"))
assert.True(t, isBinaryExt("model.onnx"))
assert.True(t, isBinaryExt("font.otf"))
assert.True(t, isBinaryExt("data.parquet"))
assert.True(t, isBinaryExt("archive.xz"))
assert.True(t, isBinaryExt("package.deb"))
assert.True(t, isBinaryExt("movie.mkv"))
assert.False(t, isBinaryExt("main.go"))
assert.False(t, isBinaryExt("config.yaml"))
assert.False(t, isBinaryExt("script.py"))
}