package sources import ( "context" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/salvacybersec/keyhunter/pkg/providers" "github.com/salvacybersec/keyhunter/pkg/recon" ) func bitbucketTestRegistry() *providers.Registry { return providers.NewRegistryFromProviders([]providers.Provider{ {Name: "openai", Keywords: []string{"sk-proj-"}}, }) } func newBitbucketSource(baseURL, token, workspace string) *BitbucketSource { return &BitbucketSource{ Token: token, Workspace: workspace, BaseURL: baseURL, Registry: bitbucketTestRegistry(), Limiters: recon.NewLimiterRegistry(), } } func TestBitbucket_EnabledRequiresTokenAndWorkspace(t *testing.T) { cfg := recon.Config{} if newBitbucketSource("", "", "").Enabled(cfg) { t.Fatal("expected disabled when token+workspace empty") } if newBitbucketSource("", "tok", "").Enabled(cfg) { t.Fatal("expected disabled when workspace empty") } if newBitbucketSource("", "", "ws").Enabled(cfg) { t.Fatal("expected disabled when token empty") } if !newBitbucketSource("", "tok", "ws").Enabled(cfg) { t.Fatal("expected enabled when both set") } } func TestBitbucket_SweepEmitsFindings(t *testing.T) { var gotAuth, gotPath string var gotQuery string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") gotPath = r.URL.Path gotQuery = r.URL.Query().Get("search_query") w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "values": [ { "content_match_count": 2, "file": {"path": "secrets/.env", "commit": {"hash": "deadbeef"}}, "page_url": "https://bitbucket.org/testws/repo/src/deadbeef/secrets/.env" } ] }`)) })) t.Cleanup(srv.Close) src := newBitbucketSource(srv.URL, "tok", "testws") out := make(chan recon.Finding, 16) if err := src.Sweep(context.Background(), "", out); err != nil { t.Fatalf("Sweep: %v", err) } close(out) if gotAuth != "Bearer tok" { t.Errorf("Authorization header = %q, want Bearer tok", gotAuth) } if gotPath != "/2.0/workspaces/testws/search/code" { t.Errorf("path = %q", gotPath) } if gotQuery == "" { t.Errorf("expected search_query param to be set") } var findings []recon.Finding for f := range out { findings = append(findings, f) } if len(findings) == 0 { t.Fatal("expected at least 1 finding") } f := findings[0] if f.SourceType != "recon:bitbucket" { t.Errorf("SourceType = %q", f.SourceType) } if !strings.Contains(f.Source, "bitbucket.org/testws/repo") { t.Errorf("Source = %q", f.Source) } } func TestBitbucket_Unauthorized(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "nope", http.StatusUnauthorized) })) t.Cleanup(srv.Close) src := newBitbucketSource(srv.URL, "tok", "testws") out := make(chan recon.Finding, 4) err := src.Sweep(context.Background(), "", out) if !errors.Is(err, ErrUnauthorized) { t.Fatalf("err = %v, want ErrUnauthorized", err) } } func TestBitbucket_ContextCancellation(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(2 * time.Second) w.WriteHeader(200) _, _ = w.Write([]byte(`{"values":[]}`)) })) t.Cleanup(srv.Close) src := newBitbucketSource(srv.URL, "tok", "testws") ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() out := make(chan recon.Finding, 1) err := src.Sweep(ctx, "", out) if err == nil { t.Fatal("expected error from cancelled context") } }