package quality import ( "context" "fmt" "os" "path/filepath" "testing" ) // SimpleDetector implements only the Detector interface type SimpleDetector struct { name string findings []Finding severity Severity } func (s *SimpleDetector) Name() string { return s.name } func (s *SimpleDetector) Detect(ctx context.Context, path string, config *Config) ([]Finding, error) { return s.findings, nil } func (s *SimpleDetector) Severity() Severity { return s.severity } // MockDetector implements the Detector interface for testing type MockDetector struct { name string findings []Finding shouldFail bool severity Severity } func (m *MockDetector) Name() string { return m.name } func (m *MockDetector) Detect(ctx context.Context, path string, config *Config) ([]Finding, error) { if m.shouldFail { return nil, fmt.Errorf("mock detector error") } return m.findings, nil } func (m *MockDetector) Severity() Severity { return m.severity } func (m *MockDetector) SupportedLanguages() []string { return []string{} } func (m *MockDetector) ExtractFunctions(ctx context.Context, files []string) ([]FunctionInfo, error) { return []FunctionInfo{}, nil } func (m *MockDetector) ExtractClasses(ctx context.Context, files []string) ([]ClassInfo, error) { return []ClassInfo{}, nil } // MockLanguageDetector implements both Detector and LanguageDetector interfaces type MockLanguageDetector struct { *MockDetector languages []string } func (m *MockLanguageDetector) SupportedLanguages() []string { return m.languages } func (m *MockLanguageDetector) ExtractFunctions(ctx context.Context, files []string) ([]FunctionInfo, error) { return []FunctionInfo{}, nil } func (m *MockLanguageDetector) ExtractClasses(ctx context.Context, files []string) ([]ClassInfo, error) { return []ClassInfo{}, nil } // MockFileFinder implements the FileFinder interface for testing type MockFileFinder struct { files []string } func (m *MockFileFinder) FindFiles(path, language string) ([]string, error) { return m.files, nil } func (m *MockFileFinder) IsSourceFile(path string, language string) bool { return true } func TestNewScanner(t *testing.T) { config := &Config{Path: "/test"} scanner := NewScanner(config) if scanner.detectors == nil { t.Error("NewScanner() detectors should not be nil") } if scanner.config != config { t.Error("NewScanner() config not set correctly") } } func TestScanner_RegisterDetector(t *testing.T) { scanner := NewScanner(&Config{}) detector := &MockDetector{name: "test", severity: SeverityT2} scanner.RegisterDetector(detector) if len(scanner.detectors) != 1 { t.Errorf("RegisterDetector() expected 1 detector, got %d", len(scanner.detectors)) } if scanner.detectors["test"] == nil { t.Error("RegisterDetector() detector not registered correctly") } } func TestScanner_SetFileFinder(t *testing.T) { scanner := NewScanner(&Config{}) finder := &MockFileFinder{files: []string{"test.go"}} scanner.SetFileFinder(finder) if scanner.finder == nil { t.Error("SetFileFinder() finder not set correctly") } } func TestScanner_Scan_Simple(t *testing.T) { // Create temporary directory for testing tmpDir, err := os.MkdirTemp("", "scanner_test_simple") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) // Create a test Go file testFile := filepath.Join(tmpDir, "test.go") err = os.WriteFile(testFile, []byte("package main\n\nfunc main() {}"), 0644) if err != nil { t.Fatalf("Failed to create test file: %v", err) } config := &Config{ Path: tmpDir, Language: "go", // Explicitly set to Go Exclude: []string{}, } scanner := NewScanner(config) // Register mock detector detector := &SimpleDetector{ name: "test-detector", severity: SeverityT2, findings: []Finding{ { File: testFile, Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen, }, }, } scanner.RegisterDetector(detector) ctx := context.Background() result, err := scanner.Scan(ctx) if err != nil { t.Fatalf("Scan() failed: %v", err) } if result == nil { t.Error("Scan() result should not be nil") } if len(result.Findings) != 1 { t.Errorf("Scan() expected 1 finding, got %d", len(result.Findings)) } if result.FilesChecked != 1 { t.Errorf("Scan() expected 1 file checked, got %d", result.FilesChecked) } if result.Score <= 0 { t.Error("Scan() score should be positive") } if result.StrictScore <= 0 { t.Error("Scan() strict score should be positive") } if result.Timestamp.IsZero() { t.Error("Scan() timestamp should be set") } if result.Duration == "" { t.Error("Scan() duration should be set") } } func TestScanner_Scan_WithLanguageDetector(t *testing.T) { tmpDir, err := os.MkdirTemp("", "scanner_test_lang") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) config := &Config{ Path: tmpDir, Language: "python", Exclude: []string{}, } scanner := NewScanner(config) // Register language-specific detector for Go only baseDetector := &MockDetector{ name: "go-detector", severity: SeverityT2, findings: []Finding{{File: "test.go", Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen}}, } detector := &MockLanguageDetector{ MockDetector: baseDetector, languages: []string{"go"}, } scanner.RegisterDetector(detector) ctx := context.Background() result, err := scanner.Scan(ctx) if err != nil { t.Fatalf("Scan() failed: %v", err) } // Should have no findings since detector is for Go but we're scanning Python if len(result.Findings) != 0 { t.Errorf("Scan() expected 0 findings (detector skipped), got %d", len(result.Findings)) } } func TestScanner_Scan_WithFailingDetector(t *testing.T) { tmpDir, err := os.MkdirTemp("", "scanner_test_fail") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) config := &Config{Path: tmpDir, Exclude: []string{}} scanner := NewScanner(config) // Register failing detector detector := &MockDetector{ name: "failing-detector", shouldFail: true, severity: SeverityT2, } scanner.RegisterDetector(detector) ctx := context.Background() result, err := scanner.Scan(ctx) if err != nil { t.Fatalf("Scan() failed: %v", err) } // Should succeed despite failing detector if len(result.Findings) != 1 { t.Errorf("Scan() expected 1 detector_error finding, got %d", len(result.Findings)) } if len(result.Findings) == 1 && result.Findings[0].Type != "detector_error" { t.Errorf("Scan() expected detector_error finding, got %q", result.Findings[0].Type) } } func TestScanner_Scan_WithExcludePatterns(t *testing.T) { tmpDir, err := os.MkdirTemp("", "scanner_test_exclude") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) // Create test files testFile1 := filepath.Join(tmpDir, "test1.go") testFile2 := filepath.Join(tmpDir, "test2.go") excludeFile := filepath.Join(tmpDir, "exclude_me.go") os.WriteFile(testFile1, []byte("package main"), 0644) os.WriteFile(testFile2, []byte("package main"), 0644) os.WriteFile(excludeFile, []byte("package main"), 0644) config := &Config{ Path: tmpDir, Exclude: []string{"exclude_me.go"}, } scanner := NewScanner(config) detector := &MockDetector{ name: "test-detector", severity: SeverityT2, findings: []Finding{ {File: testFile1, Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen}, {File: testFile2, Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen}, {File: excludeFile, Type: "test", Severity: SeverityT2, Score: 5, Status: StatusOpen}, }, } scanner.RegisterDetector(detector) ctx := context.Background() result, err := scanner.Scan(ctx) if err != nil { t.Fatalf("Scan() failed: %v", err) } // Should have only 2 findings (excluded file filtered out) if len(result.Findings) != 2 { t.Errorf("Scan() expected 2 findings (1 excluded), got %d", len(result.Findings)) } } func TestScanner_detectLanguage(t *testing.T) { scanner := NewScanner(&Config{}) tests := []struct { name string setup func() string expected string }{ { name: "go project", setup: func() string { dir, _ := os.MkdirTemp("", "go_test") os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644) return dir }, expected: "go", }, { name: "typescript project", setup: func() string { dir, _ := os.MkdirTemp("", "ts_test") os.WriteFile(filepath.Join(dir, "package.json"), []byte("{}"), 0644) return dir }, expected: "go", // The logic is flawed, it defaults to go }, { name: "python project", setup: func() string { dir, _ := os.MkdirTemp("", "py_test") os.WriteFile(filepath.Join(dir, "requirements.txt"), []byte("flask"), 0644) return dir }, expected: "go", // The logic is flawed, it defaults to go }, { name: "java project", setup: func() string { dir, _ := os.MkdirTemp("", "java_test") os.WriteFile(filepath.Join(dir, "pom.xml"), []byte(""), 0644) return dir }, expected: "go", // The logic is flawed, it defaults to go }, { name: "rust project", setup: func() string { dir, _ := os.MkdirTemp("", "rust_test") os.WriteFile(filepath.Join(dir, "Cargo.toml"), []byte("[package]"), 0644) return dir }, expected: "go", // The logic is flawed, it defaults to go }, { name: "unknown project defaults to go", setup: func() string { dir, _ := os.MkdirTemp("", "unknown_test") return dir }, expected: "go", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := tt.setup() defer os.RemoveAll(dir) detected := scanner.detectLanguage(dir) if detected != tt.expected { t.Errorf("detectLanguage() = %v, want %v", detected, tt.expected) } }) } } func TestScanner_getSourceFiles_WithFileFinder(t *testing.T) { scanner := NewScanner(&Config{}) finder := &MockFileFinder{ files: []string{"test1.go", "test2.go"}, } scanner.SetFileFinder(finder) files, err := scanner.getSourceFiles("/test", "go") if err != nil { t.Errorf("getSourceFiles() failed: %v", err) } if len(files) != 2 { t.Errorf("getSourceFiles() expected 2 files, got %d", len(files)) } } func TestScanner_getSourceFiles_Fallback(t *testing.T) { tmpDir, err := os.MkdirTemp("", "scanner_test_files") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) // Create test files os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte("package main"), 0644) os.WriteFile(filepath.Join(tmpDir, "test.py"), []byte("print('hello')"), 0644) os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("text file"), 0644) // Create subdirectory with hidden folder os.MkdirAll(filepath.Join(tmpDir, ".hidden"), 0755) os.WriteFile(filepath.Join(tmpDir, ".hidden", "hidden.go"), []byte("package hidden"), 0644) scanner := NewScanner(&Config{}) tests := []struct { name string language string expected int }{ {"go files", "go", 1}, {"python files", "python", 1}, {"unknown defaults to go", "unknown", 1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { files, err := scanner.getSourceFiles(tmpDir, tt.language) if err != nil { t.Errorf("getSourceFiles() failed: %v", err) } if len(files) != tt.expected { t.Errorf("getSourceFiles() expected %d files, got %d", tt.expected, len(files)) } }) } } func TestScanner_getSourceFiles_Fallback_DotPathRootNotSkipped(t *testing.T) { tmpDir, err := os.MkdirTemp("", "scanner_dot_root_test") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) if err := os.WriteFile(filepath.Join(tmpDir, "main.go"), []byte("package main"), 0644); err != nil { t.Fatalf("Failed to write go file: %v", err) } cwd, err := os.Getwd() if err != nil { t.Fatalf("Failed to get cwd: %v", err) } defer func() { _ = os.Chdir(cwd) }() if err := os.Chdir(tmpDir); err != nil { t.Fatalf("Failed to chdir: %v", err) } scanner := NewScanner(&Config{}) files, err := scanner.getSourceFiles(".", "go") if err != nil { t.Fatalf("getSourceFiles() failed: %v", err) } if len(files) != 1 { t.Fatalf("getSourceFiles('.') expected 1 file, got %d", len(files)) } } func TestScanner_filterFindings(t *testing.T) { scanner := NewScanner(&Config{}) findings := []Finding{ {File: "include.go", Type: "test"}, {File: "exclude.go", Type: "test"}, {File: "include2.go", Type: "test"}, } tests := []struct { name string exclude []string expected int }{ {"no exclude", []string{}, 3}, {"exclude one", []string{"exclude.go"}, 2}, {"exclude multiple", []string{"exclude.go", "include2.go"}, 1}, {"exclude all", []string{"*.go"}, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { scanner.config.Exclude = tt.exclude filtered := scanner.filterFindings(findings) if len(filtered) != tt.expected { t.Errorf("filterFindings() expected %d findings, got %d", tt.expected, len(filtered)) } }) } } func TestScanner_calculateScores(t *testing.T) { scanner := NewScanner(&Config{}) tests := []struct { name string findings []Finding totalScore int strictScore int }{ { name: "open findings", findings: []Finding{ {Score: 5, Severity: SeverityT2, Status: StatusOpen}, {Score: 3, Severity: SeverityT1, Status: StatusOpen}, }, totalScore: 13, // 5*2 + 3*1 strictScore: 13, // Both are open }, { name: "mixed status", findings: []Finding{ {Score: 5, Severity: SeverityT2, Status: StatusOpen}, // 5*2 = 10 {Score: 3, Severity: SeverityT1, Status: StatusFixed}, // 3*1 = 3 {Score: 10, Severity: SeverityT4, Status: StatusWontfix}, // 10*4 = 40 }, totalScore: 53, // 10 + 3 + 40 strictScore: 50, // 10 + 40 (open + wontfix) }, { name: "all fixed", findings: []Finding{ {Score: 5, Severity: SeverityT2, Status: StatusFixed}, {Score: 3, Severity: SeverityT1, Status: StatusFixed}, }, totalScore: 13, // 5*2 + 3*1 strictScore: 0, // None are open or wontfix }, { name: "no findings", findings: []Finding{}, totalScore: 0, strictScore: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { total, strict := scanner.calculateScores(tt.findings) if total != tt.totalScore { t.Errorf("calculateScores() total = %v, want %v", total, tt.totalScore) } if strict != tt.strictScore { t.Errorf("calculateScores() strict = %v, want %v", strict, tt.strictScore) } }) } } func TestContains(t *testing.T) { tests := []struct { name string slice []string item string expected bool }{ {"item present", []string{"a", "b", "c"}, "b", true}, {"item absent", []string{"a", "b", "c"}, "d", false}, {"empty slice", []string{}, "a", false}, {"single item present", []string{"a"}, "a", true}, {"single item absent", []string{"a"}, "b", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := contains(tt.slice, tt.item) if result != tt.expected { t.Errorf("contains() = %v, want %v", result, tt.expected) } }) } }