//go:build integration // +build integration package core import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/karust/openserp/core/fpcheck" "github.com/karust/openserp/core/fpcheck/detectors" "github.com/karust/openserp/testutil" ) const botFingerprintTestsEnv = "OPENSERP_BOT_TESTS" const botFingerprintArtifactDir = "testdata" func TestCreateBrowser(t *testing.T) { testutil.RequireIntegration(t) opts := BrowserOpts{IsHeadless: true, IsLeakless: false} browser, err := NewBrowser(opts) if err != nil { t.Fatalf("Error failed initializing browser: %s", err) } page, err := browser.Navigate(context.Background(), "about:blank") if err != nil { t.Fatalf("navigate about:blank: %v", err) } defer closeTestBrowser(t, browser) defer func() { if err := ClosePageWithTimeout(context.Background(), page, time.Second); err != nil { t.Logf("close page: %v", err) } }() } func TestNavigateUsesIsolatedBrowserContext(t *testing.T) { testutil.RequireIntegration(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/cookies/set": http.SetCookie(w, &http.Cookie{ Name: "openserp_session", Value: "request-a", Path: "/", }) _, _ = w.Write([]byte("cookie-set")) case "/cookies": _, _ = w.Write([]byte(r.Header.Get("Cookie"))) default: http.NotFound(w, r) } })) defer srv.Close() opts := BrowserOpts{IsHeadless: true, IsLeakless: false, Timeout: 15 * time.Second} browser, err := NewBrowser(opts) if err != nil { t.Fatalf("failed initializing browser: %s", err) } defer closeTestBrowser(t, browser) pageA, err := browser.Navigate(context.Background(), srv.URL+"/cookies/set") if err != nil { t.Fatalf("navigate cookie setter: %v", err) } if err := ClosePageWithTimeout(context.Background(), pageA, time.Second); err != nil { t.Fatalf("close setter page: %v", err) } pageB, err := browser.Navigate(context.Background(), srv.URL+"/cookies") if err != nil { t.Fatalf("navigate cookie reader: %v", err) } defer func() { if err := ClosePageWithTimeout(context.Background(), pageB, time.Second); err != nil { t.Logf("close reader page: %v", err) } }() body, err := pageB.Timeout(5 * time.Second).Element("body") if err != nil { t.Fatalf("read response body: %v", err) } cookieHeader, err := body.Text() if err != nil { t.Fatalf("extract response text: %v", err) } if strings.Contains(cookieHeader, "openserp_session=request-a") { t.Fatalf("cookie leaked between requests; got header %q", cookieHeader) } } func TestNavigateReusesCookiesForSameProxyLane(t *testing.T) { testutil.RequireIntegration(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/cookies/set": http.SetCookie(w, &http.Cookie{Name: "openserp_lane", Value: "same-lane", Path: "/"}) _, _ = w.Write([]byte("cookie-set")) case "/cookies": _, _ = w.Write([]byte(r.Header.Get("Cookie"))) default: http.NotFound(w, r) } })) defer srv.Close() browser, err := NewBrowser(BrowserOpts{ IsHeadless: true, IsLeakless: false, Timeout: 15 * time.Second, ProxyLaneStore: NewLaneStore(10), }) if err != nil { t.Fatalf("failed initializing browser: %s", err) } defer closeTestBrowser(t, browser) laneCtx := WithProxyLaneKey(WithEngine(context.Background(), "google"), ProxyLaneKey{Engine: "google", SessionID: "sid-a"}) pageA, err := browser.Navigate(laneCtx, srv.URL+"/cookies/set") if err != nil { t.Fatalf("navigate cookie setter: %v", err) } if err := ClosePageWithTimeout(context.Background(), pageA, time.Second); err != nil { t.Fatalf("close setter page: %v", err) } pageB, err := browser.Navigate(laneCtx, srv.URL+"/cookies") if err != nil { t.Fatalf("navigate cookie reader: %v", err) } defer func() { if err := ClosePageWithTimeout(context.Background(), pageB, time.Second); err != nil { t.Logf("close reader page: %v", err) } }() body, err := pageB.Timeout(5 * time.Second).Element("body") if err != nil { t.Fatalf("read response body: %v", err) } cookieHeader, err := body.Text() if err != nil { t.Fatalf("extract response text: %v", err) } if !strings.Contains(cookieHeader, "openserp_lane=same-lane") { t.Fatalf("expected same proxy lane to restore cookie, got %q", cookieHeader) } } func TestNavigateDoesNotShareCookiesAcrossProxyLanes(t *testing.T) { testutil.RequireIntegration(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/cookies/set": http.SetCookie(w, &http.Cookie{Name: "openserp_lane", Value: "lane-a", Path: "/"}) _, _ = w.Write([]byte("cookie-set")) case "/cookies": _, _ = w.Write([]byte(r.Header.Get("Cookie"))) default: http.NotFound(w, r) } })) defer srv.Close() browser, err := NewBrowser(BrowserOpts{ IsHeadless: true, IsLeakless: false, Timeout: 15 * time.Second, ProxyLaneStore: NewLaneStore(10), }) if err != nil { t.Fatalf("failed initializing browser: %s", err) } defer closeTestBrowser(t, browser) laneA := WithProxyLaneKey(WithEngine(context.Background(), "google"), ProxyLaneKey{Engine: "google", SessionID: "sid-a"}) laneB := WithProxyLaneKey(WithEngine(context.Background(), "google"), ProxyLaneKey{Engine: "google", SessionID: "sid-b"}) pageA, err := browser.Navigate(laneA, srv.URL+"/cookies/set") if err != nil { t.Fatalf("navigate cookie setter: %v", err) } if err := ClosePageWithTimeout(context.Background(), pageA, time.Second); err != nil { t.Fatalf("close setter page: %v", err) } pageB, err := browser.Navigate(laneB, srv.URL+"/cookies") if err != nil { t.Fatalf("navigate cookie reader: %v", err) } defer func() { if err := ClosePageWithTimeout(context.Background(), pageB, time.Second); err != nil { t.Logf("close reader page: %v", err) } }() body, err := pageB.Timeout(5 * time.Second).Element("body") if err != nil { t.Fatalf("read response body: %v", err) } cookieHeader, err := body.Text() if err != nil { t.Fatalf("extract response text: %v", err) } if strings.Contains(cookieHeader, "openserp_lane=lane-a") { t.Fatalf("cookie leaked across proxy lanes; got header %q", cookieHeader) } } func TestNavigateClassifiesMainDocumentStatus(t *testing.T) { testutil.RequireIntegration(t) tests := []struct { name string status int want error }{ {name: "blocked", status: http.StatusForbidden, want: ErrBlocked}, {name: "rate limited", status: http.StatusTooManyRequests, want: ErrRateLimited}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(tt.status) _, _ = w.Write([]byte("classified")) })) defer srv.Close() browser, err := NewBrowser(BrowserOpts{IsHeadless: true, IsLeakless: false, Timeout: 15 * time.Second}) if err != nil { t.Fatalf("failed initializing browser: %s", err) } defer closeTestBrowser(t, browser) page, err := browser.Navigate(context.Background(), srv.URL) if page != nil { _ = ClosePageWithTimeout(context.Background(), page, time.Second) } if !errors.Is(err, tt.want) { t.Fatalf("expected %v, got %v", tt.want, err) } }) } } func TestFingerprintDetectors(t *testing.T) { testutil.RequireIntegration(t) if strings.TrimSpace(os.Getenv(botFingerprintTestsEnv)) != "1" { t.Skipf("set %s=1 to run fingerprint tests", botFingerprintTestsEnv) } reports := make(map[string]fpcheck.Report) criticalFailures := make([]string, 0) for _, detector := range detectors.All() { report := runFingerprintDetector(t, detector) key := detector.Name() reports[key] = report if len(report.Summary.Critical) > 0 { for _, critical := range report.Summary.Critical { criticalFailures = append(criticalFailures, fmt.Sprintf("%s:%s", key, critical)) } } } reportPath := filepath.Join(botFingerprintArtifactDir, "fingerprint_report.json") if err := writeFingerprintReport(reportPath, reports); err != nil { t.Fatalf("write fingerprint report: %v", err) } absReportPath, err := filepath.Abs(reportPath) if err == nil { t.Logf("Fingerprint report artifact: %s", absReportPath) } else { t.Logf("Fingerprint report artifact: core/%s", filepath.ToSlash(reportPath)) } if len(criticalFailures) > 0 { t.Fatalf("critical fingerprint detections found: %s", strings.Join(criticalFailures, ", ")) } } func runFingerprintDetector(t *testing.T, detector fpcheck.Detector) fpcheck.Report { t.Helper() opts := BrowserOpts{ IsHeadless: true, IsLeakless: false, Timeout: 20 * time.Second, } browser, err := NewBrowser(opts) if err != nil { t.Fatalf("create browser: %v", err) } defer closeTestBrowser(t, browser) report, err := fpcheck.Run(context.Background(), browser, detector, botFingerprintArtifactDir) if err != nil { t.Fatalf("run detector %s: %v", detector.Name(), err) } t.Logf( "Fingerprint %s: passed=%d failed=%d critical=%d", detector.Name(), report.Summary.Passed, report.Summary.Failed, len(report.Summary.Critical), ) return report } func writeFingerprintReport(path string, reports map[string]fpcheck.Report) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return fmt.Errorf("create artifact directory: %w", err) } data, err := json.MarshalIndent(reports, "", " ") if err != nil { return fmt.Errorf("marshal report: %w", err) } data = append(data, '\n') if err := os.WriteFile(path, data, 0o644); err != nil { return fmt.Errorf("write report file %s: %w", path, err) } return nil } func closeTestBrowser(t *testing.T, browser *Browser) { t.Helper() if browser == nil { return } if err := browser.Close(); err != nil { t.Logf("close browser: %v", err) } }