package dorks import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) func sampleDork() Dork { return Dork{ ID: "openai-github-envfile", Name: "OpenAI key in .env", Source: "github", Query: "sk-proj- extension:env", } } func newTestExecutor(token, baseURL string) *GitHubExecutor { return &GitHubExecutor{ Token: token, BaseURL: baseURL, HTTPClient: &http.Client{Timeout: 5 * time.Second}, MaxRetries: 1, } } func TestGitHubExecutor_Source(t *testing.T) { g := NewGitHubExecutor("x") if g.Source() != "github" { t.Fatalf("expected source %q, got %q", "github", g.Source()) } } func TestGitHubExecutor_MissingTokenReturnsErrMissingAuth(t *testing.T) { // Use a server that would fail the test if it were ever hit — the // executor must short-circuit before issuing an HTTP request. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatalf("server should not be hit when token is empty; got %s %s", r.Method, r.URL.Path) })) defer srv.Close() g := newTestExecutor("", srv.URL) _, err := g.Execute(context.Background(), sampleDork(), 10) if err == nil { t.Fatal("expected error for empty token") } if !errors.Is(err, ErrMissingAuth) { t.Fatalf("expected ErrMissingAuth, got %v", err) } if !strings.Contains(err.Error(), "GITHUB_TOKEN") { t.Fatalf("expected setup instructions in error, got %q", err.Error()) } } func TestGitHubExecutor_SuccessfulSearchParsesMatches(t *testing.T) { body := `{ "total_count": 2, "items": [ { "name": ".env", "path": "backend/.env", "html_url": "https://github.com/acme/leaky/blob/main/backend/.env", "repository": {"full_name": "acme/leaky"}, "text_matches": [{"fragment": "OPENAI_API_KEY=sk-proj-AAA"}] }, { "name": "config.env", "path": "infra/config.env", "html_url": "https://github.com/acme/other/blob/main/infra/config.env", "repository": {"full_name": "acme/other"}, "text_matches": [{"fragment": "SECRET=sk-proj-BBB"}] } ] }` srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("Authorization"); got != "Bearer test-token" { t.Errorf("expected Bearer auth, got %q", got) } if got := r.Header.Get("Accept"); !strings.Contains(got, "text-match") { t.Errorf("expected text-match Accept header, got %q", got) } if r.URL.Path != "/search/code" { t.Errorf("expected /search/code, got %s", r.URL.Path) } if q := r.URL.Query().Get("q"); q != "sk-proj- extension:env" { t.Errorf("expected query to be url-decoded to original, got %q", q) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) })) defer srv.Close() g := newTestExecutor("test-token", srv.URL) matches, err := g.Execute(context.Background(), sampleDork(), 10) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(matches) != 2 { t.Fatalf("expected 2 matches, got %d", len(matches)) } m := matches[0] if m.DorkID != "openai-github-envfile" { t.Errorf("DorkID = %q", m.DorkID) } if m.Source != "github" { t.Errorf("Source = %q", m.Source) } if m.URL != "https://github.com/acme/leaky/blob/main/backend/.env" { t.Errorf("URL = %q", m.URL) } if m.Path != "acme/leaky/backend/.env" { t.Errorf("Path = %q", m.Path) } if m.Snippet != "OPENAI_API_KEY=sk-proj-AAA" { t.Errorf("Snippet = %q", m.Snippet) } } func TestGitHubExecutor_LimitCapsResults(t *testing.T) { // Build a response with 10 items; executor must return only 5. var items []string for i := 0; i < 10; i++ { items = append(items, fmt.Sprintf(`{ "name": "f%d.env", "path": "dir/f%d.env", "html_url": "https://github.com/acme/repo/blob/main/dir/f%d.env", "repository": {"full_name": "acme/repo"}, "text_matches": [{"fragment": "sk-proj-%d"}] }`, i, i, i, i)) } body := fmt.Sprintf(`{"total_count": 10, "items": [%s]}`, strings.Join(items, ",")) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got := r.URL.Query().Get("per_page"); got != "5" { t.Errorf("expected per_page=5, got %q", got) } w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(body)) })) defer srv.Close() g := newTestExecutor("test-token", srv.URL) matches, err := g.Execute(context.Background(), sampleDork(), 5) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(matches) != 5 { t.Fatalf("expected 5 matches after cap, got %d", len(matches)) } } func TestGitHubExecutor_RetryAfterSleepsAndRetries(t *testing.T) { var hits int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { n := atomic.AddInt32(&hits, 1) if n == 1 { w.Header().Set("Retry-After", "1") w.Header().Set("X-RateLimit-Remaining", "0") w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"message":"rate limit"}`)) return } w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"total_count":1,"items":[{ "name":"a.env","path":"a.env","html_url":"https://github.com/x/y/blob/main/a.env", "repository":{"full_name":"x/y"}, "text_matches":[{"fragment":"sk-proj-ZZ"}]}]}`)) })) defer srv.Close() g := newTestExecutor("test-token", srv.URL) start := time.Now() matches, err := g.Execute(context.Background(), sampleDork(), 10) elapsed := time.Since(start) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(matches) != 1 { t.Fatalf("expected 1 match after retry, got %d", len(matches)) } if atomic.LoadInt32(&hits) != 2 { t.Fatalf("expected 2 server hits, got %d", hits) } if elapsed < 900*time.Millisecond { t.Fatalf("expected to sleep ~1s from Retry-After, only waited %s", elapsed) } } func TestGitHubExecutor_RateLimitExhaustedReturnsError(t *testing.T) { var hits int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&hits, 1) w.Header().Set("Retry-After", "0") w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"message":"secondary rate limit"}`)) })) defer srv.Close() g := newTestExecutor("test-token", srv.URL) g.MaxRetries = 1 _, err := g.Execute(context.Background(), sampleDork(), 10) if err == nil { t.Fatal("expected rate limit error") } if !strings.Contains(err.Error(), "rate limit") { t.Fatalf("expected rate limit in error, got %q", err.Error()) } if atomic.LoadInt32(&hits) != 2 { t.Fatalf("expected 2 hits (initial + 1 retry), got %d", hits) } } func TestGitHubExecutor_UnauthorizedMapsToMissingAuth(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"message":"Bad credentials"}`)) })) defer srv.Close() g := newTestExecutor("bad-token", srv.URL) _, err := g.Execute(context.Background(), sampleDork(), 10) if err == nil { t.Fatal("expected error") } if !errors.Is(err, ErrMissingAuth) { t.Fatalf("expected ErrMissingAuth wrap, got %v", err) } } func TestGitHubExecutor_UnprocessableEntityReturnsDescriptiveError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) _, _ = w.Write([]byte(`{"message":"Validation Failed"}`)) })) defer srv.Close() g := newTestExecutor("test-token", srv.URL) _, err := g.Execute(context.Background(), sampleDork(), 10) if err == nil { t.Fatal("expected error") } if !strings.Contains(err.Error(), "422") { t.Fatalf("expected status code in error, got %q", err.Error()) } if !strings.Contains(err.Error(), "Validation Failed") { t.Fatalf("expected server message in error, got %q", err.Error()) } } func TestParseRetryAfter(t *testing.T) { cases := map[string]time.Duration{ "": time.Second, "0": time.Second, "1": time.Second, "5": 5 * time.Second, "huh?": time.Second, } for in, want := range cases { if got := parseRetryAfter(in); got != want { t.Errorf("parseRetryAfter(%q) = %s, want %s", in, got, want) } } }