package scanner import ( "os" "path/filepath" "reflect" "sort" "strings" "testing" "god-eye/internal/config" ) func TestLoadWordlist(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "wordlist.txt") content := `# comment line api admin # another comment dev staging test ` if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } got, err := LoadWordlist(path) if err != nil { t.Fatalf("unexpected error: %v", err) } want := []string{"api", "admin", "dev", "staging", "test"} if !reflect.DeepEqual(got, want) { t.Errorf("got %v, want %v", got, want) } } func TestLoadWordlist_NonExistent(t *testing.T) { _, err := LoadWordlist("/tmp/this-does-not-exist-xyz-abc.txt") if err == nil { t.Error("expected error for non-existent file") } } func TestLoadWordlist_Empty(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "empty.txt") os.WriteFile(path, []byte(""), 0o644) got, err := LoadWordlist(path) if err != nil { t.Fatal(err) } if len(got) != 0 { t.Errorf("expected empty result, got %v", got) } } func TestLoadWordlist_CommentsOnly(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "comments.txt") os.WriteFile(path, []byte("# only comments\n# and more\n"), 0o644) got, _ := LoadWordlist(path) if len(got) != 0 { t.Errorf("expected empty result for comments-only file, got %v", got) } } func TestParseResolvers(t *testing.T) { tests := []struct { name string in string want []string }{ { name: "empty uses defaults", in: "", want: config.DefaultResolvers, }, { name: "single with port", in: "8.8.8.8:53", want: []string{"8.8.8.8:53"}, }, { name: "single without port adds :53", in: "8.8.8.8", want: []string{"8.8.8.8:53"}, }, { name: "multiple with mixed ports", in: "8.8.8.8,1.1.1.1:5353,9.9.9.9", want: []string{"8.8.8.8:53", "1.1.1.1:5353", "9.9.9.9:53"}, }, { name: "whitespace trimmed", in: " 8.8.8.8 , 1.1.1.1 ", want: []string{"8.8.8.8:53", "1.1.1.1:53"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ParseResolvers(tt.in) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ParseResolvers(%q) = %v, want %v", tt.in, got, tt.want) } }) } } func TestParsePorts(t *testing.T) { tests := []struct { name string in string want []int }{ {"empty uses defaults", "", []int{80, 443, 8080, 8443}}, {"single valid", "80", []int{80}}, {"multiple valid", "80,443,3000", []int{80, 443, 3000}}, {"whitespace", " 80 , 443 ", []int{80, 443}}, {"invalid silently dropped", "80,abc,443", []int{80, 443}}, {"out of range dropped", "80,99999,443", []int{80, 443}}, {"negative dropped", "80,-1,443", []int{80, 443}}, {"zero dropped", "0,80,443", []int{80, 443}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := ParsePorts(tt.in) if !reflect.DeepEqual(got, tt.want) { t.Errorf("ParsePorts(%q) = %v, want %v", tt.in, got, tt.want) } }) } } func TestCountActive(t *testing.T) { results := map[string]*config.SubdomainResult{ "a.example.com": {StatusCode: 200}, "b.example.com": {StatusCode: 301}, "c.example.com": {StatusCode: 404}, "d.example.com": {StatusCode: 500}, "e.example.com": {StatusCode: 0}, // not probed } got := countActive(results) if got != 2 { t.Errorf("countActive = %d, want 2", got) } } func TestCountVulns(t *testing.T) { results := map[string]*config.SubdomainResult{ "a.example.com": {OpenRedirect: true}, "b.example.com": {CORSMisconfig: "wildcard with credentials"}, "c.example.com": {DangerousMethods: []string{"PUT", "DELETE"}}, "d.example.com": {GitExposed: true}, "e.example.com": {BackupFiles: []string{"backup.sql"}}, "f.example.com": {StatusCode: 200}, // clean } got := countVulns(results) if got != 5 { t.Errorf("countVulns = %d, want 5", got) } } func TestCountSubdomainsWithAI(t *testing.T) { results := map[string]*config.SubdomainResult{ "a.example.com": {AIFindings: []string{"finding1"}}, "b.example.com": {AIFindings: []string{"f1", "f2"}}, "c.example.com": {}, // no AI findings } got := countSubdomainsWithAI(results) if got != 2 { t.Errorf("countSubdomainsWithAI = %d, want 2", got) } } func TestBuildAISummary(t *testing.T) { results := map[string]*config.SubdomainResult{ "a.example.com": { AIFindings: []string{"Hardcoded API key", "Weak crypto"}, AISeverity: "critical", CVEFindings: []string{"CVE-2021-12345"}, }, "b.example.com": { AIFindings: []string{"Missing CSP"}, AISeverity: "medium", }, "c.example.com": { AIFindings: []string{"ignored"}, AISeverity: "info", }, } got := buildAISummary(results) if got == "" { t.Fatal("summary is empty") } // Must mention severities mustContain := []string{"critical", "high", "medium", "CRITICAL", "MEDIUM", "Hardcoded API key", "CVE-2021-12345"} for _, s := range mustContain { if !strings.Contains(got, s) { t.Errorf("summary missing expected token %q in:\n%s", s, got) } } } func TestSortedIntsInvariant(t *testing.T) { // Sanity: whenever we sort ints we expect ascending order (tests ScanPorts sorting guarantee). in := []int{443, 80, 8080, 22} sort.Ints(in) if !sort.IntsAreSorted(in) { t.Error("sort.IntsAreSorted returned false after sort.Ints") } }