| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package middleware |
| |
| import ( |
| "fmt" |
| "io/ioutil" |
| "net/http" |
| "net/http/httptest" |
| "strconv" |
| "testing" |
| "time" |
| |
| "github.com/alicebob/miniredis/v2" |
| "github.com/go-redis/redis/v8" |
| "github.com/google/go-cmp/cmp" |
| "go.opencensus.io/stats/view" |
| "golang.org/x/pkgsite/internal/config" |
| ) |
| |
| func TestCache(t *testing.T) { |
| // force cache writes to be synchronous |
| TestMode = true |
| // These variables are mutated before each test case to control the handler |
| // response. |
| var ( |
| body string |
| status int |
| ) |
| handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| if status > 0 { |
| w.WriteHeader(status) |
| } |
| fmt.Fprint(w, body) |
| }) |
| |
| s, err := miniredis.Run() |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer s.Close() |
| |
| c := redis.NewClient(&redis.Options{Addr: s.Addr()}) |
| mux := http.NewServeMux() |
| mux.Handle("/A", Cache("A", c, TTL(1*time.Minute), []string{"yes"})(handler)) |
| mux.Handle("/B", handler) |
| ts := httptest.NewServer(mux) |
| view.Register(CacheResultCount) |
| // The following tests are stateful: the result of each test depends on the |
| // state in redis resulting from all tests before it. |
| tests := []struct { |
| label string |
| advanceTime time.Duration |
| path string |
| body string |
| status int |
| bypass bool |
| wantHitCounts map[bool]int |
| wantBody string |
| wantStatus int |
| }{ |
| { |
| label: "first failure", |
| path: "A", |
| body: "1", |
| status: http.StatusInternalServerError, |
| wantHitCounts: map[bool]int{false: 1}, |
| wantBody: "1", |
| wantStatus: http.StatusInternalServerError, |
| }, |
| { |
| label: "first success", |
| path: "A", |
| body: "2", |
| status: http.StatusOK, |
| wantHitCounts: map[bool]int{false: 2}, |
| wantBody: "2", |
| wantStatus: http.StatusOK, |
| }, |
| { |
| label: "B is uncached", |
| advanceTime: 10 * time.Second, |
| path: "B", |
| body: "3", |
| status: http.StatusForbidden, |
| wantHitCounts: map[bool]int{false: 2}, |
| wantBody: "3", |
| wantStatus: http.StatusForbidden, |
| }, |
| { |
| label: "A is cached", |
| path: "A", |
| // These shouldn't matter, since we'll hit the cache. |
| body: "3", |
| status: http.StatusForbidden, |
| wantHitCounts: map[bool]int{false: 2, true: 1}, |
| wantBody: "2", |
| wantStatus: http.StatusOK, |
| }, |
| { |
| label: "cache expires", |
| path: "A", |
| // with the ten seconds above, this should expire the 1 minute cache. |
| advanceTime: 1 * time.Minute, |
| body: "4", |
| // status is the zero value, but caching should still trigger. |
| wantHitCounts: map[bool]int{false: 3, true: 1}, |
| wantBody: "4", |
| wantStatus: http.StatusOK, |
| }, |
| { |
| label: "A is cached again", |
| path: "A", |
| // 30 seconds is not enough time to expire the cache. |
| advanceTime: 30 * time.Second, |
| body: "5", |
| wantHitCounts: map[bool]int{false: 3, true: 2}, |
| wantBody: "4", |
| wantStatus: http.StatusOK, |
| }, |
| { |
| label: "bypassing the cache", |
| path: "A", |
| body: "6", |
| bypass: true, |
| // hitCounts should not be modified. |
| wantHitCounts: map[bool]int{false: 3, true: 2}, |
| wantBody: "6", |
| wantStatus: http.StatusOK, |
| }, |
| } |
| |
| for _, test := range tests { |
| s.FastForward(test.advanceTime) |
| body = test.body |
| status = test.status |
| req, err := http.NewRequest("GET", ts.URL+"/"+test.path, nil) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if test.bypass { |
| req.Header.Set(config.BypassCacheAuthHeader, "yes") |
| } |
| resp, err := ts.Client().Do(req) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp.StatusCode != test.wantStatus { |
| t.Errorf("[%s] GET returned status %d, want %d", test.label, resp.StatusCode, test.wantStatus) |
| } |
| body, err := ioutil.ReadAll(resp.Body) |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp.Body.Close() |
| if string(body) != test.wantBody { |
| t.Errorf("[%s] GET returned body %s, want %s", test.label, string(body), test.wantBody) |
| } |
| rows, err := view.RetrieveData(CacheResultCount.Name) |
| if err != nil { |
| t.Fatal(err) |
| } |
| hitCounts := make(map[bool]int) |
| for _, row := range rows { |
| // Tags[0] should always be the hit result (true or false). For |
| // simplicity we assume this. |
| source, err := strconv.ParseBool(row.Tags[0].Value) |
| if err != nil { |
| t.Fatal(err) |
| } |
| count := int(row.Data.(*view.CountData).Value) |
| hitCounts[source] = count |
| } |
| if diff := cmp.Diff(test.wantHitCounts, hitCounts); diff != "" { |
| t.Errorf("[%s] CacheResultCount diff (-want +got):\n%s", test.label, diff) |
| } |
| } |
| } |