mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-11-30 18:20:19 +00:00
1476 lines
34 KiB
Go
1476 lines
34 KiB
Go
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")
|
|
}
|
|
}
|