package fa import ( "context" "net/http" "net/http/httptest" "strings" "sync" "testing" ) // TestRequestOverride_PerCallCookiesAndUA exercises the multi-tenant flow: // one Client built with default creds, two calls in a row each carrying a // different user's cookies via per-call Options. The server records the // Cookie + User-Agent headers it saw on each request; the per-call values // must override the client's, and the client's defaults must come back on // a call that passes no overrides. func TestRequestOverride_PerCallCookiesAndUA(t *testing.T) { type seen struct { cookie string userAgent string } var ( mu sync.Mutex hits []seen ) mux := http.NewServeMux() mux.HandleFunc("/view/1/", func(w http.ResponseWriter, r *http.Request) { mu.Lock() hits = append(hits, seen{cookie: r.Header.Get("Cookie"), userAgent: r.Header.Get("User-Agent")}) mu.Unlock() w.Header().Set("Content-Type", "text/html") _, _ = w.Write([]byte(syntheticSubmissionHTML)) }) srv := httptest.NewServer(mux) defer srv.Close() // Client built with default ("system") creds plus a default UA. client := newE2EClient(t, srv) // Layer the client default cookies in via WithCookies so we can verify // they appear when no override is passed. clientWithDefaults := New( WithHTTPClient(client.http), WithRateLimit(0, 16), WithMaxRetries(0), WithUserAgent("default-ua/1.0"), WithCookies(Cookies{A: "defaultA", B: "defaultB"}), ) ctx := context.Background() // Call 1: no override → client defaults must apply. if _, err := clientWithDefaults.GetSubmission(ctx, 1); err != nil { t.Fatalf("call 1: %v", err) } // Call 2: per-user override (user-A creds + user-A UA). if _, err := clientWithDefaults.GetSubmission(ctx, 1, WithCookies(Cookies{A: "userA_a", B: "userA_b"}), WithCloudflare(CFCookies{Clearance: "userA_cf"}), WithUserAgent("userA-browser/1.0"), ); err != nil { t.Fatalf("call 2: %v", err) } // Call 3: a different user. if _, err := clientWithDefaults.GetSubmission(ctx, 1, WithCookies(Cookies{A: "userB_a", B: "userB_b"}), ); err != nil { t.Fatalf("call 3: %v", err) } mu.Lock() defer mu.Unlock() if len(hits) != 3 { t.Fatalf("hits = %d; want 3", len(hits)) } // Call 1: default cookies + default UA. if !strings.Contains(hits[0].cookie, "a=defaultA") || !strings.Contains(hits[0].cookie, "b=defaultB") { t.Errorf("call 1 cookie = %q; want default a/b", hits[0].cookie) } if hits[0].userAgent != "default-ua/1.0" { t.Errorf("call 1 UA = %q; want default-ua/1.0", hits[0].userAgent) } // Call 2: user-A creds replace the jar's defaults wholesale. if !strings.Contains(hits[1].cookie, "a=userA_a") || !strings.Contains(hits[1].cookie, "b=userA_b") { t.Errorf("call 2 cookie = %q; want user-A a/b", hits[1].cookie) } if !strings.Contains(hits[1].cookie, "cf_clearance=userA_cf") { t.Errorf("call 2 cookie missing cf_clearance: %q", hits[1].cookie) } if strings.Contains(hits[1].cookie, "defaultA") || strings.Contains(hits[1].cookie, "defaultB") { t.Errorf("call 2 cookie leaked client defaults: %q", hits[1].cookie) } if hits[1].userAgent != "userA-browser/1.0" { t.Errorf("call 2 UA = %q; want userA-browser/1.0", hits[1].userAgent) } // Call 3: user-B cookies override, but UA falls back to client default // because the override did not touch it. if !strings.Contains(hits[2].cookie, "a=userB_a") || !strings.Contains(hits[2].cookie, "b=userB_b") { t.Errorf("call 3 cookie = %q; want user-B a/b", hits[2].cookie) } if hits[2].userAgent != "default-ua/1.0" { t.Errorf("call 3 UA = %q; want default-ua/1.0 (no override)", hits[2].userAgent) } } // TestRequestOverride_NoOverrideMeansNoCtxValue is a cheap sanity check // that applyRequestOptions short-circuits when nothing request-level // actually changed (e.g. caller passed only client-only options). func TestRequestOverride_NoOverrideMeansNoCtxValue(t *testing.T) { c := New(WithCookies(Cookies{A: "x", B: "y"})) ctx := context.Background() // No options at all → same ctx. if got := c.applyRequestOptions(ctx, nil); got != ctx { t.Error("nil opts should pass through ctx unchanged") } // A client-only option that does not touch request-level fields. got := c.applyRequestOptions(ctx, []Option{WithMaxRetries(7)}) if got != ctx { t.Error("client-only option should not attach a request override") } // A request-level option that happens to equal the current value. got = c.applyRequestOptions(ctx, []Option{WithCookies(Cookies{A: "x", B: "y"})}) if got != ctx { t.Error("override matching client config should not attach") } // A real override. got = c.applyRequestOptions(ctx, []Option{WithCookies(Cookies{A: "z", B: "w"})}) if got == ctx { t.Error("real override should produce a new ctx") } if ov := requestOverrideFrom(got); ov == nil || ov.cookies.A != "z" { t.Errorf("override ctx missing or wrong: %+v", ov) } }