| package httpcache |
| |
| import ( |
| "bytes" |
| "errors" |
| "flag" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "net/http/httptest" |
| "os" |
| "strconv" |
| "testing" |
| "time" |
| ) |
| |
| var s struct { |
| server *httptest.Server |
| client http.Client |
| transport *Transport |
| done chan struct{} // Closed to unlock infinite handlers. |
| } |
| |
| type fakeClock struct { |
| elapsed time.Duration |
| } |
| |
| func (c *fakeClock) since(t time.Time) time.Duration { |
| return c.elapsed |
| } |
| |
| func TestMain(m *testing.M) { |
| flag.Parse() |
| setup() |
| code := m.Run() |
| teardown() |
| os.Exit(code) |
| } |
| |
| func setup() { |
| tp := NewMemoryCacheTransport() |
| client := http.Client{Transport: tp} |
| s.transport = tp |
| s.client = client |
| s.done = make(chan struct{}) |
| |
| mux := http.NewServeMux() |
| s.server = httptest.NewServer(mux) |
| |
| mux.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Cache-Control", "max-age=3600") |
| })) |
| |
| mux.HandleFunc("/method", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Cache-Control", "max-age=3600") |
| w.Write([]byte(r.Method)) |
| })) |
| |
| mux.HandleFunc("/range", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| lm := "Fri, 14 Dec 2010 01:01:50 GMT" |
| if r.Header.Get("if-modified-since") == lm { |
| w.WriteHeader(http.StatusNotModified) |
| return |
| } |
| w.Header().Set("last-modified", lm) |
| if r.Header.Get("range") == "bytes=4-9" { |
| w.WriteHeader(http.StatusPartialContent) |
| w.Write([]byte(" text ")) |
| return |
| } |
| w.Write([]byte("Some text content")) |
| })) |
| |
| mux.HandleFunc("/nostore", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Cache-Control", "no-store") |
| })) |
| |
| mux.HandleFunc("/etag", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| etag := "124567" |
| if r.Header.Get("if-none-match") == etag { |
| w.WriteHeader(http.StatusNotModified) |
| return |
| } |
| w.Header().Set("etag", etag) |
| })) |
| |
| mux.HandleFunc("/lastmodified", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| lm := "Fri, 14 Dec 2010 01:01:50 GMT" |
| if r.Header.Get("if-modified-since") == lm { |
| w.WriteHeader(http.StatusNotModified) |
| return |
| } |
| w.Header().Set("last-modified", lm) |
| })) |
| |
| mux.HandleFunc("/varyaccept", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Cache-Control", "max-age=3600") |
| w.Header().Set("Content-Type", "text/plain") |
| w.Header().Set("Vary", "Accept") |
| w.Write([]byte("Some text content")) |
| })) |
| |
| mux.HandleFunc("/doublevary", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Cache-Control", "max-age=3600") |
| w.Header().Set("Content-Type", "text/plain") |
| w.Header().Set("Vary", "Accept, Accept-Language") |
| w.Write([]byte("Some text content")) |
| })) |
| mux.HandleFunc("/2varyheaders", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Cache-Control", "max-age=3600") |
| w.Header().Set("Content-Type", "text/plain") |
| w.Header().Add("Vary", "Accept") |
| w.Header().Add("Vary", "Accept-Language") |
| w.Write([]byte("Some text content")) |
| })) |
| mux.HandleFunc("/varyunused", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Cache-Control", "max-age=3600") |
| w.Header().Set("Content-Type", "text/plain") |
| w.Header().Set("Vary", "X-Madeup-Header") |
| w.Write([]byte("Some text content")) |
| })) |
| |
| mux.HandleFunc("/cachederror", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| etag := "abc" |
| if r.Header.Get("if-none-match") == etag { |
| w.WriteHeader(http.StatusNotModified) |
| return |
| } |
| w.Header().Set("etag", etag) |
| w.WriteHeader(http.StatusNotFound) |
| w.Write([]byte("Not found")) |
| })) |
| |
| updateFieldsCounter := 0 |
| mux.HandleFunc("/updatefields", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("X-Counter", strconv.Itoa(updateFieldsCounter)) |
| w.Header().Set("Etag", `"e"`) |
| updateFieldsCounter++ |
| if r.Header.Get("if-none-match") != "" { |
| w.WriteHeader(http.StatusNotModified) |
| return |
| } |
| w.Write([]byte("Some text content")) |
| })) |
| |
| // Take 3 seconds to return 200 OK (for testing client timeouts). |
| mux.HandleFunc("/3seconds", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| time.Sleep(3 * time.Second) |
| })) |
| |
| mux.HandleFunc("/infinite", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| for { |
| select { |
| case <-s.done: |
| return |
| default: |
| w.Write([]byte{0}) |
| } |
| } |
| })) |
| } |
| |
| func teardown() { |
| close(s.done) |
| s.server.Close() |
| } |
| |
| func resetTest() { |
| s.transport.Cache = NewMemoryCache() |
| clock = &realClock{} |
| } |
| |
| // TestCacheableMethod ensures that uncacheable method does not get stored |
| // in cache and get incorrectly used for a following cacheable method request. |
| func TestCacheableMethod(t *testing.T) { |
| resetTest() |
| { |
| req, err := http.NewRequest("POST", s.server.URL+"/method", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| var buf bytes.Buffer |
| _, err = io.Copy(&buf, resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = resp.Body.Close() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got, want := buf.String(), "POST"; got != want { |
| t.Errorf("got %q, want %q", got, want) |
| } |
| if resp.StatusCode != http.StatusOK { |
| t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) |
| } |
| } |
| { |
| req, err := http.NewRequest("GET", s.server.URL+"/method", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| var buf bytes.Buffer |
| _, err = io.Copy(&buf, resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = resp.Body.Close() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got, want := buf.String(), "GET"; got != want { |
| t.Errorf("got wrong body %q, want %q", got, want) |
| } |
| if resp.StatusCode != http.StatusOK { |
| t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) |
| } |
| if resp.Header.Get(XFromCache) != "" { |
| t.Errorf("XFromCache header isn't blank") |
| } |
| } |
| } |
| |
| func TestDontServeHeadResponseToGetRequest(t *testing.T) { |
| resetTest() |
| url := s.server.URL + "/" |
| req, err := http.NewRequest(http.MethodHead, url, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| _, err = s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req, err = http.NewRequest(http.MethodGet, url, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp.Header.Get(XFromCache) != "" { |
| t.Errorf("Cache should not match") |
| } |
| } |
| |
| func TestDontStorePartialRangeInCache(t *testing.T) { |
| resetTest() |
| { |
| req, err := http.NewRequest("GET", s.server.URL+"/range", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Set("range", "bytes=4-9") |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| var buf bytes.Buffer |
| _, err = io.Copy(&buf, resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = resp.Body.Close() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got, want := buf.String(), " text "; got != want { |
| t.Errorf("got %q, want %q", got, want) |
| } |
| if resp.StatusCode != http.StatusPartialContent { |
| t.Errorf("response status code isn't 206 Partial Content: %v", resp.StatusCode) |
| } |
| } |
| { |
| req, err := http.NewRequest("GET", s.server.URL+"/range", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| var buf bytes.Buffer |
| _, err = io.Copy(&buf, resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = resp.Body.Close() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got, want := buf.String(), "Some text content"; got != want { |
| t.Errorf("got %q, want %q", got, want) |
| } |
| if resp.StatusCode != http.StatusOK { |
| t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) |
| } |
| if resp.Header.Get(XFromCache) != "" { |
| t.Error("XFromCache header isn't blank") |
| } |
| } |
| { |
| req, err := http.NewRequest("GET", s.server.URL+"/range", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| var buf bytes.Buffer |
| _, err = io.Copy(&buf, resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = resp.Body.Close() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got, want := buf.String(), "Some text content"; got != want { |
| t.Errorf("got %q, want %q", got, want) |
| } |
| if resp.StatusCode != http.StatusOK { |
| t.Errorf("response status code isn't 200 OK: %v", resp.StatusCode) |
| } |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Errorf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| } |
| { |
| req, err := http.NewRequest("GET", s.server.URL+"/range", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Set("range", "bytes=4-9") |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| var buf bytes.Buffer |
| _, err = io.Copy(&buf, resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = resp.Body.Close() |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got, want := buf.String(), " text "; got != want { |
| t.Errorf("got %q, want %q", got, want) |
| } |
| if resp.StatusCode != http.StatusPartialContent { |
| t.Errorf("response status code isn't 206 Partial Content: %v", resp.StatusCode) |
| } |
| } |
| } |
| |
| func TestCacheOnlyIfBodyRead(t *testing.T) { |
| resetTest() |
| { |
| req, err := http.NewRequest("GET", s.server.URL, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| // We do not read the body |
| resp.Body.Close() |
| } |
| { |
| req, err := http.NewRequest("GET", s.server.URL, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatalf("XFromCache header isn't blank") |
| } |
| } |
| } |
| |
| func TestOnlyReadBodyOnDemand(t *testing.T) { |
| resetTest() |
| |
| req, err := http.NewRequest("GET", s.server.URL+"/infinite", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) // This shouldn't hang forever. |
| if err != nil { |
| t.Fatal(err) |
| } |
| buf := make([]byte, 10) // Only partially read the body. |
| _, err = resp.Body.Read(buf) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp.Body.Close() |
| } |
| |
| func TestGetOnlyIfCachedHit(t *testing.T) { |
| resetTest() |
| { |
| req, err := http.NewRequest("GET", s.server.URL, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| { |
| req, err := http.NewRequest("GET", s.server.URL, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Add("cache-control", "only-if-cached") |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| if resp.StatusCode != http.StatusOK { |
| t.Fatalf("response status code isn't 200 OK: %v", resp.StatusCode) |
| } |
| } |
| } |
| |
| func TestGetOnlyIfCachedMiss(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Add("cache-control", "only-if-cached") |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| if resp.StatusCode != http.StatusGatewayTimeout { |
| t.Fatalf("response status code isn't 504 GatewayTimeout: %v", resp.StatusCode) |
| } |
| } |
| |
| func TestGetNoStoreRequest(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Add("Cache-Control", "no-store") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| } |
| |
| func TestGetNoStoreResponse(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL+"/nostore", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| } |
| |
| func TestGetWithEtag(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL+"/etag", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| // additional assertions to verify that 304 response is converted properly |
| if resp.StatusCode != http.StatusOK { |
| t.Fatalf("response status code isn't 200 OK: %v", resp.StatusCode) |
| } |
| if _, ok := resp.Header["Connection"]; ok { |
| t.Fatalf("Connection header isn't absent") |
| } |
| } |
| } |
| |
| func TestGetWithLastModified(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL+"/lastmodified", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| } |
| } |
| |
| func TestGetWithVary(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL+"/varyaccept", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Set("Accept", "text/plain") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get("Vary") != "Accept" { |
| t.Fatalf(`Vary header isn't "Accept": %v`, resp.Header.Get("Vary")) |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| } |
| req.Header.Set("Accept", "text/html") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| req.Header.Set("Accept", "") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| } |
| |
| func TestGetWithDoubleVary(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL+"/doublevary", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Set("Accept", "text/plain") |
| req.Header.Set("Accept-Language", "da, en-gb;q=0.8, en;q=0.7") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get("Vary") == "" { |
| t.Fatalf(`Vary header is blank`) |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| } |
| req.Header.Set("Accept-Language", "") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| req.Header.Set("Accept-Language", "da") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| } |
| |
| func TestGetWith2VaryHeaders(t *testing.T) { |
| resetTest() |
| // Tests that multiple Vary headers' comma-separated lists are |
| // merged. See https://github.com/gregjones/httpcache/issues/27. |
| const ( |
| accept = "text/plain" |
| acceptLanguage = "da, en-gb;q=0.8, en;q=0.7" |
| ) |
| req, err := http.NewRequest("GET", s.server.URL+"/2varyheaders", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Set("Accept", accept) |
| req.Header.Set("Accept-Language", acceptLanguage) |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get("Vary") == "" { |
| t.Fatalf(`Vary header is blank`) |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| } |
| req.Header.Set("Accept-Language", "") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| req.Header.Set("Accept-Language", "da") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| req.Header.Set("Accept-Language", acceptLanguage) |
| req.Header.Set("Accept", "") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| } |
| req.Header.Set("Accept", "image/png") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "" { |
| t.Fatal("XFromCache header isn't blank") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| } |
| } |
| |
| func TestGetVaryUnused(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL+"/varyunused", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| req.Header.Set("Accept", "text/plain") |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get("Vary") == "" { |
| t.Fatalf(`Vary header is blank`) |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| } |
| } |
| |
| func TestUpdateFields(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL+"/updatefields", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| var counter, counter2 string |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| counter = resp.Header.Get("x-counter") |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.Header.Get(XFromCache) != "1" { |
| t.Fatalf(`XFromCache header isn't "1": %v`, resp.Header.Get(XFromCache)) |
| } |
| counter2 = resp.Header.Get("x-counter") |
| } |
| if counter == counter2 { |
| t.Fatalf(`both "x-counter" values are equal: %v %v`, counter, counter2) |
| } |
| } |
| |
| // This tests the fix for https://github.com/gregjones/httpcache/issues/74. |
| // Previously, after validating a cached response, its StatusCode |
| // was incorrectly being replaced. |
| func TestCachedErrorsKeepStatus(t *testing.T) { |
| resetTest() |
| req, err := http.NewRequest("GET", s.server.URL+"/cachederror", nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| io.Copy(ioutil.Discard, resp.Body) |
| } |
| { |
| resp, err := s.client.Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer resp.Body.Close() |
| if resp.StatusCode != http.StatusNotFound { |
| t.Fatalf("Status code isn't 404: %d", resp.StatusCode) |
| } |
| } |
| } |
| |
| func TestParseCacheControl(t *testing.T) { |
| resetTest() |
| h := http.Header{} |
| for range parseCacheControl(h) { |
| t.Fatal("cacheControl should be empty") |
| } |
| |
| h.Set("cache-control", "no-cache") |
| { |
| cc := parseCacheControl(h) |
| if _, ok := cc["foo"]; ok { |
| t.Error(`Value "foo" shouldn't exist`) |
| } |
| noCache, ok := cc["no-cache"] |
| if !ok { |
| t.Fatalf(`"no-cache" value isn't set`) |
| } |
| if noCache != "" { |
| t.Fatalf(`"no-cache" value isn't blank: %v`, noCache) |
| } |
| } |
| h.Set("cache-control", "no-cache, max-age=3600") |
| { |
| cc := parseCacheControl(h) |
| noCache, ok := cc["no-cache"] |
| if !ok { |
| t.Fatalf(`"no-cache" value isn't set`) |
| } |
| if noCache != "" { |
| t.Fatalf(`"no-cache" value isn't blank: %v`, noCache) |
| } |
| if cc["max-age"] != "3600" { |
| t.Fatalf(`"max-age" value isn't "3600": %v`, cc["max-age"]) |
| } |
| } |
| } |
| |
| func TestNoCacheRequestExpiration(t *testing.T) { |
| resetTest() |
| respHeaders := http.Header{} |
| respHeaders.Set("Cache-Control", "max-age=7200") |
| |
| reqHeaders := http.Header{} |
| reqHeaders.Set("Cache-Control", "no-cache") |
| if getFreshness(respHeaders, reqHeaders) != transparent { |
| t.Fatal("freshness isn't transparent") |
| } |
| } |
| |
| func TestNoCacheResponseExpiration(t *testing.T) { |
| resetTest() |
| respHeaders := http.Header{} |
| respHeaders.Set("Cache-Control", "no-cache") |
| respHeaders.Set("Expires", "Wed, 19 Apr 3000 11:43:00 GMT") |
| |
| reqHeaders := http.Header{} |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func TestReqMustRevalidate(t *testing.T) { |
| resetTest() |
| // not paying attention to request setting max-stale means never returning stale |
| // responses, so always acting as if must-revalidate is set |
| respHeaders := http.Header{} |
| |
| reqHeaders := http.Header{} |
| reqHeaders.Set("Cache-Control", "must-revalidate") |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func TestRespMustRevalidate(t *testing.T) { |
| resetTest() |
| respHeaders := http.Header{} |
| respHeaders.Set("Cache-Control", "must-revalidate") |
| |
| reqHeaders := http.Header{} |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func TestFreshExpiration(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| respHeaders := http.Header{} |
| respHeaders.Set("date", now.Format(time.RFC1123)) |
| respHeaders.Set("expires", now.Add(time.Duration(2)*time.Second).Format(time.RFC1123)) |
| |
| reqHeaders := http.Header{} |
| if getFreshness(respHeaders, reqHeaders) != fresh { |
| t.Fatal("freshness isn't fresh") |
| } |
| |
| clock = &fakeClock{elapsed: 3 * time.Second} |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func TestMaxAge(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| respHeaders := http.Header{} |
| respHeaders.Set("date", now.Format(time.RFC1123)) |
| respHeaders.Set("cache-control", "max-age=2") |
| |
| reqHeaders := http.Header{} |
| if getFreshness(respHeaders, reqHeaders) != fresh { |
| t.Fatal("freshness isn't fresh") |
| } |
| |
| clock = &fakeClock{elapsed: 3 * time.Second} |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func TestMaxAgeZero(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| respHeaders := http.Header{} |
| respHeaders.Set("date", now.Format(time.RFC1123)) |
| respHeaders.Set("cache-control", "max-age=0") |
| |
| reqHeaders := http.Header{} |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func TestBothMaxAge(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| respHeaders := http.Header{} |
| respHeaders.Set("date", now.Format(time.RFC1123)) |
| respHeaders.Set("cache-control", "max-age=2") |
| |
| reqHeaders := http.Header{} |
| reqHeaders.Set("cache-control", "max-age=0") |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func TestMinFreshWithExpires(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| respHeaders := http.Header{} |
| respHeaders.Set("date", now.Format(time.RFC1123)) |
| respHeaders.Set("expires", now.Add(time.Duration(2)*time.Second).Format(time.RFC1123)) |
| |
| reqHeaders := http.Header{} |
| reqHeaders.Set("cache-control", "min-fresh=1") |
| if getFreshness(respHeaders, reqHeaders) != fresh { |
| t.Fatal("freshness isn't fresh") |
| } |
| |
| reqHeaders = http.Header{} |
| reqHeaders.Set("cache-control", "min-fresh=2") |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func TestEmptyMaxStale(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| respHeaders := http.Header{} |
| respHeaders.Set("date", now.Format(time.RFC1123)) |
| respHeaders.Set("cache-control", "max-age=20") |
| |
| reqHeaders := http.Header{} |
| reqHeaders.Set("cache-control", "max-stale") |
| clock = &fakeClock{elapsed: 10 * time.Second} |
| if getFreshness(respHeaders, reqHeaders) != fresh { |
| t.Fatal("freshness isn't fresh") |
| } |
| |
| clock = &fakeClock{elapsed: 60 * time.Second} |
| if getFreshness(respHeaders, reqHeaders) != fresh { |
| t.Fatal("freshness isn't fresh") |
| } |
| } |
| |
| func TestMaxStaleValue(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| respHeaders := http.Header{} |
| respHeaders.Set("date", now.Format(time.RFC1123)) |
| respHeaders.Set("cache-control", "max-age=10") |
| |
| reqHeaders := http.Header{} |
| reqHeaders.Set("cache-control", "max-stale=20") |
| clock = &fakeClock{elapsed: 5 * time.Second} |
| if getFreshness(respHeaders, reqHeaders) != fresh { |
| t.Fatal("freshness isn't fresh") |
| } |
| |
| clock = &fakeClock{elapsed: 15 * time.Second} |
| if getFreshness(respHeaders, reqHeaders) != fresh { |
| t.Fatal("freshness isn't fresh") |
| } |
| |
| clock = &fakeClock{elapsed: 30 * time.Second} |
| if getFreshness(respHeaders, reqHeaders) != stale { |
| t.Fatal("freshness isn't stale") |
| } |
| } |
| |
| func containsHeader(headers []string, header string) bool { |
| for _, v := range headers { |
| if http.CanonicalHeaderKey(v) == http.CanonicalHeaderKey(header) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func TestGetEndToEndHeaders(t *testing.T) { |
| resetTest() |
| var ( |
| headers http.Header |
| end2end []string |
| ) |
| |
| headers = http.Header{} |
| headers.Set("content-type", "text/html") |
| headers.Set("te", "deflate") |
| |
| end2end = getEndToEndHeaders(headers) |
| if !containsHeader(end2end, "content-type") { |
| t.Fatal(`doesn't contain "content-type" header`) |
| } |
| if containsHeader(end2end, "te") { |
| t.Fatal(`doesn't contain "te" header`) |
| } |
| |
| headers = http.Header{} |
| headers.Set("connection", "content-type") |
| headers.Set("content-type", "text/csv") |
| headers.Set("te", "deflate") |
| end2end = getEndToEndHeaders(headers) |
| if containsHeader(end2end, "connection") { |
| t.Fatal(`doesn't contain "connection" header`) |
| } |
| if containsHeader(end2end, "content-type") { |
| t.Fatal(`doesn't contain "content-type" header`) |
| } |
| if containsHeader(end2end, "te") { |
| t.Fatal(`doesn't contain "te" header`) |
| } |
| |
| headers = http.Header{} |
| end2end = getEndToEndHeaders(headers) |
| if len(end2end) != 0 { |
| t.Fatal(`non-zero end2end headers`) |
| } |
| |
| headers = http.Header{} |
| headers.Set("connection", "content-type") |
| end2end = getEndToEndHeaders(headers) |
| if len(end2end) != 0 { |
| t.Fatal(`non-zero end2end headers`) |
| } |
| } |
| |
| type transportMock struct { |
| response *http.Response |
| err error |
| } |
| |
| func (t transportMock) RoundTrip(req *http.Request) (resp *http.Response, err error) { |
| return t.response, t.err |
| } |
| |
| func TestStaleIfErrorRequest(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| tmock := transportMock{ |
| response: &http.Response{ |
| Status: http.StatusText(http.StatusOK), |
| StatusCode: http.StatusOK, |
| Header: http.Header{ |
| "Date": []string{now.Format(time.RFC1123)}, |
| "Cache-Control": []string{"no-cache"}, |
| }, |
| Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), |
| }, |
| err: nil, |
| } |
| tp := NewMemoryCacheTransport() |
| tp.Transport = &tmock |
| |
| // First time, response is cached on success |
| r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) |
| r.Header.Set("Cache-Control", "stale-if-error") |
| resp, err := tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // On failure, response is returned from the cache |
| tmock.response = nil |
| tmock.err = errors.New("some error") |
| resp, err = tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| } |
| |
| func TestStaleIfErrorRequestLifetime(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| tmock := transportMock{ |
| response: &http.Response{ |
| Status: http.StatusText(http.StatusOK), |
| StatusCode: http.StatusOK, |
| Header: http.Header{ |
| "Date": []string{now.Format(time.RFC1123)}, |
| "Cache-Control": []string{"no-cache"}, |
| }, |
| Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), |
| }, |
| err: nil, |
| } |
| tp := NewMemoryCacheTransport() |
| tp.Transport = &tmock |
| |
| // First time, response is cached on success |
| r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) |
| r.Header.Set("Cache-Control", "stale-if-error=100") |
| resp, err := tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // On failure, response is returned from the cache |
| tmock.response = nil |
| tmock.err = errors.New("some error") |
| resp, err = tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| |
| // Same for http errors |
| tmock.response = &http.Response{StatusCode: http.StatusInternalServerError} |
| tmock.err = nil |
| resp, err = tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| |
| // If failure last more than max stale, error is returned |
| clock = &fakeClock{elapsed: 200 * time.Second} |
| _, err = tp.RoundTrip(r) |
| if err != tmock.err { |
| t.Fatalf("got err %v, want %v", err, tmock.err) |
| } |
| } |
| |
| func TestStaleIfErrorResponse(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| tmock := transportMock{ |
| response: &http.Response{ |
| Status: http.StatusText(http.StatusOK), |
| StatusCode: http.StatusOK, |
| Header: http.Header{ |
| "Date": []string{now.Format(time.RFC1123)}, |
| "Cache-Control": []string{"no-cache, stale-if-error"}, |
| }, |
| Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), |
| }, |
| err: nil, |
| } |
| tp := NewMemoryCacheTransport() |
| tp.Transport = &tmock |
| |
| // First time, response is cached on success |
| r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) |
| resp, err := tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // On failure, response is returned from the cache |
| tmock.response = nil |
| tmock.err = errors.New("some error") |
| resp, err = tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| } |
| |
| func TestStaleIfErrorResponseLifetime(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| tmock := transportMock{ |
| response: &http.Response{ |
| Status: http.StatusText(http.StatusOK), |
| StatusCode: http.StatusOK, |
| Header: http.Header{ |
| "Date": []string{now.Format(time.RFC1123)}, |
| "Cache-Control": []string{"no-cache, stale-if-error=100"}, |
| }, |
| Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), |
| }, |
| err: nil, |
| } |
| tp := NewMemoryCacheTransport() |
| tp.Transport = &tmock |
| |
| // First time, response is cached on success |
| r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) |
| resp, err := tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // On failure, response is returned from the cache |
| tmock.response = nil |
| tmock.err = errors.New("some error") |
| resp, err = tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| |
| // If failure last more than max stale, error is returned |
| clock = &fakeClock{elapsed: 200 * time.Second} |
| _, err = tp.RoundTrip(r) |
| if err != tmock.err { |
| t.Fatalf("got err %v, want %v", err, tmock.err) |
| } |
| } |
| |
| // This tests the fix for https://github.com/gregjones/httpcache/issues/74. |
| // Previously, after a stale response was used after encountering an error, |
| // its StatusCode was being incorrectly replaced. |
| func TestStaleIfErrorKeepsStatus(t *testing.T) { |
| resetTest() |
| now := time.Now() |
| tmock := transportMock{ |
| response: &http.Response{ |
| Status: http.StatusText(http.StatusNotFound), |
| StatusCode: http.StatusNotFound, |
| Header: http.Header{ |
| "Date": []string{now.Format(time.RFC1123)}, |
| "Cache-Control": []string{"no-cache"}, |
| }, |
| Body: ioutil.NopCloser(bytes.NewBuffer([]byte("some data"))), |
| }, |
| err: nil, |
| } |
| tp := NewMemoryCacheTransport() |
| tp.Transport = &tmock |
| |
| // First time, response is cached on success |
| r, _ := http.NewRequest("GET", "http://somewhere.com/", nil) |
| r.Header.Set("Cache-Control", "stale-if-error") |
| resp, err := tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| _, err = ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // On failure, response is returned from the cache |
| tmock.response = nil |
| tmock.err = errors.New("some error") |
| resp, err = tp.RoundTrip(r) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("resp is nil") |
| } |
| if resp.StatusCode != http.StatusNotFound { |
| t.Fatalf("Status wasn't 404: %d", resp.StatusCode) |
| } |
| } |
| |
| // Test that http.Client.Timeout is respected when cache transport is used. |
| // That is so as long as request cancellation is propagated correctly. |
| // In the past, that required CancelRequest to be implemented correctly, |
| // but modern http.Client uses Request.Cancel (or request context) instead, |
| // so we don't have to do anything. |
| func TestClientTimeout(t *testing.T) { |
| if testing.Short() { |
| t.Skip("skipping timeout test in short mode") // Because it takes at least 3 seconds to run. |
| } |
| resetTest() |
| client := &http.Client{ |
| Transport: NewMemoryCacheTransport(), |
| Timeout: time.Second, |
| } |
| started := time.Now() |
| resp, err := client.Get(s.server.URL + "/3seconds") |
| taken := time.Since(started) |
| if err == nil { |
| t.Error("got nil error, want timeout error") |
| } |
| if resp != nil { |
| t.Error("got non-nil resp, want nil resp") |
| } |
| if taken >= 2*time.Second { |
| t.Error("client.Do took 2+ seconds, want < 2 seconds") |
| } |
| } |