package transport import ( "context" "net/http" "net/http/httptest" "testing" ) func TestFetchBodyIfChangedPrefersETagAndTreats304AsUnchanged(t *testing.T) { t.Helper() var call int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { call++ switch call { case 1: if got := r.Header.Get("If-None-Match"); got != "" { t.Fatalf("first request If-None-Match = %q, want empty", got) } if got := r.Header.Get("If-Modified-Since"); got != "" { t.Fatalf("first request If-Modified-Since = %q, want empty", got) } w.Header().Set("ETag", `"v1"`) w.Header().Set("Last-Modified", "Mon, 02 Jan 2006 15:04:05 GMT") _, _ = w.Write([]byte(`{"ok":true}`)) case 2: if got := r.Header.Get("If-None-Match"); got != `"v1"` { t.Fatalf("second request If-None-Match = %q, want %q", got, `"v1"`) } if got := r.Header.Get("If-Modified-Since"); got != "" { t.Fatalf("second request If-Modified-Since = %q, want empty when ETag is cached", got) } w.WriteHeader(http.StatusNotModified) default: t.Fatalf("unexpected call count %d", call) } })) defer srv.Close() validators := HTTPValidators{} body, changed, next, err := FetchBodyIfChanged(context.Background(), srv.Client(), srv.URL, "test-agent", "application/json", true, validators) if err != nil { t.Fatalf("first FetchBodyIfChanged() error = %v", err) } if !changed { t.Fatalf("first FetchBodyIfChanged() changed = false, want true") } if got := string(body); got != `{"ok":true}` { t.Fatalf("first FetchBodyIfChanged() body = %q", got) } if got := next.ETag; got != `"v1"` { t.Fatalf("cached ETag = %q, want %q", got, `"v1"`) } if got := next.LastModified; got != "Mon, 02 Jan 2006 15:04:05 GMT" { t.Fatalf("cached Last-Modified = %q", got) } body, changed, next, err = FetchBodyIfChanged(context.Background(), srv.Client(), srv.URL, "test-agent", "application/json", true, next) if err != nil { t.Fatalf("second FetchBodyIfChanged() error = %v", err) } if changed { t.Fatalf("second FetchBodyIfChanged() changed = true, want false") } if body != nil { t.Fatalf("second FetchBodyIfChanged() body = %q, want nil", string(body)) } if got := next.ETag; got != `"v1"` { t.Fatalf("cached ETag after 304 = %q, want %q", got, `"v1"`) } } func TestFetchBodyIfChangedFallsBackToIfModifiedSince(t *testing.T) { t.Helper() var call int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { call++ switch call { case 1: w.Header().Set("Last-Modified", "Tue, 03 Jan 2006 15:04:05 GMT") _, _ = w.Write([]byte(`first`)) case 2: if got := r.Header.Get("If-None-Match"); got != "" { t.Fatalf("second request If-None-Match = %q, want empty", got) } if got := r.Header.Get("If-Modified-Since"); got != "Tue, 03 Jan 2006 15:04:05 GMT" { t.Fatalf("second request If-Modified-Since = %q", got) } w.WriteHeader(http.StatusNotModified) default: t.Fatalf("unexpected call count %d", call) } })) defer srv.Close() _, changed, validators, err := FetchBodyIfChanged(context.Background(), srv.Client(), srv.URL, "test-agent", "", true, HTTPValidators{}) if err != nil { t.Fatalf("first FetchBodyIfChanged() error = %v", err) } if !changed { t.Fatalf("first FetchBodyIfChanged() changed = false, want true") } if got := validators.LastModified; got != "Tue, 03 Jan 2006 15:04:05 GMT" { t.Fatalf("cached Last-Modified = %q", got) } _, changed, _, err = FetchBodyIfChanged(context.Background(), srv.Client(), srv.URL, "test-agent", "", true, validators) if err != nil { t.Fatalf("second FetchBodyIfChanged() error = %v", err) } if changed { t.Fatalf("second FetchBodyIfChanged() changed = true, want false") } } func TestFetchBodyIfChangedClearsValidatorsOn200WithoutValidators(t *testing.T) { t.Helper() var call int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { call++ switch call { case 1: w.Header().Set("ETag", `"v1"`) _, _ = w.Write([]byte(`first`)) case 2: if got := r.Header.Get("If-None-Match"); got != `"v1"` { t.Fatalf("second request If-None-Match = %q", got) } _, _ = w.Write([]byte(`second`)) case 3: if got := r.Header.Get("If-None-Match"); got != "" { t.Fatalf("third request If-None-Match = %q, want empty", got) } if got := r.Header.Get("If-Modified-Since"); got != "" { t.Fatalf("third request If-Modified-Since = %q, want empty", got) } _, _ = w.Write([]byte(`third`)) default: t.Fatalf("unexpected call count %d", call) } })) defer srv.Close() _, _, validators, err := FetchBodyIfChanged(context.Background(), srv.Client(), srv.URL, "test-agent", "", true, HTTPValidators{}) if err != nil { t.Fatalf("first FetchBodyIfChanged() error = %v", err) } _, _, validators, err = FetchBodyIfChanged(context.Background(), srv.Client(), srv.URL, "test-agent", "", true, validators) if err != nil { t.Fatalf("second FetchBodyIfChanged() error = %v", err) } if validators.ETag != "" || validators.LastModified != "" { t.Fatalf("validators after 200 without validators = %+v, want cleared", validators) } _, _, _, err = FetchBodyIfChanged(context.Background(), srv.Client(), srv.URL, "test-agent", "", true, validators) if err != nil { t.Fatalf("third FetchBodyIfChanged() error = %v", err) } } func TestFetchBodyIfChangedConditionalDisabledSkipsConditionalHeaders(t *testing.T) { t.Helper() var calls int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ if got := r.Header.Get("If-None-Match"); got != "" { t.Fatalf("request If-None-Match = %q, want empty", got) } if got := r.Header.Get("If-Modified-Since"); got != "" { t.Fatalf("request If-Modified-Since = %q, want empty", got) } _, _ = w.Write([]byte(`body`)) })) defer srv.Close() validators := HTTPValidators{ETag: `"v1"`, LastModified: "Wed, 04 Jan 2006 15:04:05 GMT"} _, changed, next, err := FetchBodyIfChanged(context.Background(), srv.Client(), srv.URL, "test-agent", "", false, validators) if err != nil { t.Fatalf("FetchBodyIfChanged() error = %v", err) } if !changed { t.Fatalf("FetchBodyIfChanged() changed = false, want true") } if next != validators { t.Fatalf("validators changed when conditional disabled: got %+v want %+v", next, validators) } if calls != 1 { t.Fatalf("calls = %d, want 1", calls) } } func TestFetchBodyIfChangedAllowsEmpty304ButRejectsEmpty200(t *testing.T) { t.Helper() notModified := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotModified) })) defer notModified.Close() _, changed, _, err := FetchBodyIfChanged( context.Background(), notModified.Client(), notModified.URL, "test-agent", "", true, HTTPValidators{ETag: `"v1"`}, ) if err != nil { t.Fatalf("304 FetchBodyIfChanged() error = %v", err) } if changed { t.Fatalf("304 FetchBodyIfChanged() changed = true, want false") } emptyBody := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer emptyBody.Close() _, _, _, err = FetchBodyIfChanged(context.Background(), emptyBody.Client(), emptyBody.URL, "test-agent", "", true, HTTPValidators{}) if err == nil { t.Fatalf("empty 200 FetchBodyIfChanged() error = nil, want error") } if err.Error() != "empty response body" { t.Fatalf("empty 200 FetchBodyIfChanged() error = %q", err) } }