package sources import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "golang.org/x/time/rate" "github.com/salvacybersec/keyhunter/pkg/providers" "github.com/salvacybersec/keyhunter/pkg/recon" ) func newCodebergTestRegistry() *providers.Registry { return providers.NewRegistryFromProviders([]providers.Provider{ { Name: "openai", DisplayName: "OpenAI", Tier: 1, Keywords: []string{"sk-proj-"}, FormatVersion: 1, }, }) } func TestCodebergSource_NameAndInterface(t *testing.T) { var _ recon.ReconSource = (*CodebergSource)(nil) s := &CodebergSource{} if got := s.Name(); got != "codeberg" { t.Errorf("Name() = %q, want %q", got, "codeberg") } if s.RespectsRobots() { t.Errorf("RespectsRobots() = true, want false") } if !s.Enabled(recon.Config{}) { t.Errorf("Enabled() = false, want true (public API)") } } func TestCodebergSource_RateLimitUnauthenticated(t *testing.T) { s := &CodebergSource{} got := s.RateLimit() want := rate.Every(60 * time.Second) if got != want { t.Errorf("RateLimit() no token = %v, want %v", got, want) } if s.Burst() != 1 { t.Errorf("Burst() = %d, want 1", s.Burst()) } } func TestCodebergSource_RateLimitAuthenticated(t *testing.T) { s := &CodebergSource{Token: "abc123"} got := s.RateLimit() want := rate.Every(3600 * time.Millisecond) if got != want { t.Errorf("RateLimit() with token = %v, want %v", got, want) } } func TestCodebergSource_SweepEmitsFindings(t *testing.T) { var gotAuth string var 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("q") w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "ok": true, "data": []map[string]any{ { "full_name": "alice/leaked-keys", "html_url": "https://codeberg.org/alice/leaked-keys", }, }, }) })) defer srv.Close() s := &CodebergSource{ BaseURL: srv.URL, Registry: newCodebergTestRegistry(), Limiters: recon.NewLimiterRegistry(), } out := make(chan recon.Finding, 8) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Sweep(ctx, "", out); err != nil { t.Fatalf("Sweep: %v", err) } close(out) var findings []recon.Finding for f := range out { findings = append(findings, f) } if len(findings) == 0 { t.Fatalf("expected at least one finding") } f := findings[0] if f.Source != "https://codeberg.org/alice/leaked-keys" { t.Errorf("Source = %q, want codeberg html_url", f.Source) } if f.SourceType != "recon:codeberg" { t.Errorf("SourceType = %q, want recon:codeberg", f.SourceType) } if f.ProviderName != "openai" { t.Errorf("ProviderName = %q, want openai", f.ProviderName) } if gotPath != "/api/v1/repos/search" { t.Errorf("path = %q, want /api/v1/repos/search", gotPath) } if gotQuery == "" { t.Errorf("query param empty") } if gotAuth != "" { t.Errorf("Authorization header should be absent without token, got %q", gotAuth) } } func TestCodebergSource_SweepWithTokenSetsAuthHeader(t *testing.T) { var gotAuth string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"ok":true,"data":[]}`)) })) defer srv.Close() s := &CodebergSource{ Token: "s3cret", BaseURL: srv.URL, Registry: newCodebergTestRegistry(), Limiters: recon.NewLimiterRegistry(), } out := make(chan recon.Finding, 1) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := s.Sweep(ctx, "", out); err != nil { t.Fatalf("Sweep: %v", err) } if !strings.HasPrefix(gotAuth, "token ") || !strings.Contains(gotAuth, "s3cret") { t.Errorf("Authorization header = %q, want \"token s3cret\"", gotAuth) } } func TestCodebergSource_SweepContextCancellation(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { select { case <-r.Context().Done(): case <-time.After(3 * time.Second): } })) defer srv.Close() s := &CodebergSource{ BaseURL: srv.URL, Registry: newCodebergTestRegistry(), Limiters: recon.NewLimiterRegistry(), } ctx, cancel := context.WithCancel(context.Background()) cancel() out := make(chan recon.Finding, 1) err := s.Sweep(ctx, "", out) if err == nil { t.Fatalf("expected error on cancelled context") } }