app/apicache_stale_test.go
132 lines · 3777 bytes
1package app
2
3import (
4 "errors"
5 "net/http"
6 "testing"
7 "time"
8)
9
10// failingRT is an upstream that can be switched between a scripted status and
11// a transport error mid-test.
12type failingRT struct {
13 fakeRT
14 err error
15}
16
17func (f *failingRT) RoundTrip(r *http.Request) (*http.Response, error) {
18 f.mu.Lock()
19 err := f.err
20 f.mu.Unlock()
21 if err != nil {
22 f.mu.Lock()
23 f.calls++
24 f.mu.Unlock()
25 return nil, err
26 }
27 return f.fakeRT.RoundTrip(r)
28}
29
30func (f *failingRT) set(status int, body string, err error) {
31 f.mu.Lock()
32 defer f.mu.Unlock()
33 f.status, f.body, f.err = status, body, err
34}
35
36// staleCache returns a cache with a one minute TTL and one hour stale window
37// over a controllable clock, warmed with one good response for puppyURL.
38func staleCache(t *testing.T) (*apiCache, *failingRT, http.RoundTripper, *time.Time) {
39 t.Helper()
40 up := &failingRT{}
41 up.set(200, `{"good":1}`, nil)
42 c := newAPICache(1<<20, time.Minute, time.Hour)
43 now := time.Now()
44 c.now = func() time.Time { return now }
45 rt := c.transport(up)
46 get(t, rt, puppyURL)
47 return c, up, rt, &now
48}
49
50func TestStaleEntryServedWhenUpstreamBlocks(t *testing.T) {
51 c, up, rt, now := staleCache(t)
52 *now = now.Add(2 * time.Minute) // past ttl, inside the stale window
53 up.set(403, "<html>blocked</html>", nil)
54
55 status, body := get(t, rt, puppyURL)
56
57 if status != 200 || body != `{"good":1}` {
58 t.Errorf("got %d %q, want the stale 200 body", status, body)
59 }
60 if up.count() != 2 {
61 t.Errorf("upstream called %d times, want 2: one warm-up and one attempt that hit the block", up.count())
62 }
63 if _, _, stale, _, _ := c.stats(); stale != 1 {
64 t.Errorf("stale counter is %d, want 1", stale)
65 }
66}
67
68func TestStaleEntryServedOnTransportError(t *testing.T) {
69 _, up, rt, now := staleCache(t)
70 *now = now.Add(2 * time.Minute)
71 up.set(0, "", errors.New("dial tcp: connection refused"))
72
73 status, body := get(t, rt, puppyURL)
74
75 if status != 200 || body != `{"good":1}` {
76 t.Errorf("got %d %q, want the stale 200 body", status, body)
77 }
78}
79
80func TestBlockBackoffSkipsUpstream(t *testing.T) {
81 _, up, rt, now := staleCache(t)
82 *now = now.Add(2 * time.Minute)
83 up.set(403, "<html>blocked</html>", nil)
84 get(t, rt, puppyURL) // triggers the block
85 calls := up.count()
86
87 other := "https://www.deviantart.com/_puppy/dabrowse/search/all?q=other"
88 status, body := get(t, rt, other)
89 if status != 403 || body != "<html>blocked</html>" {
90 t.Errorf("uncached key during backoff got %d %q, want the block response", status, body)
91 }
92 if up.count() != calls {
93 t.Errorf("upstream called during backoff (%d -> %d), want none", calls, up.count())
94 }
95
96 *now = now.Add(blockBackoff + time.Second)
97 up.set(200, `{"back":1}`, nil)
98 if status, _ := get(t, rt, other); status != 200 || up.count() != calls+1 {
99 t.Errorf("after backoff: status %d, calls %d, want 200 and one more upstream call", status, up.count())
100 }
101}
102
103func TestStaleEntryDroppedAfterTheWindow(t *testing.T) {
104 c, up, rt, now := staleCache(t)
105 *now = now.Add(time.Minute + time.Hour + time.Second) // past ttl and stale
106 up.set(403, "<html>blocked</html>", nil)
107
108 status, _ := get(t, rt, puppyURL)
109
110 if status != 403 {
111 t.Errorf("got %d, want the 403 passed through once nothing stale is held", status)
112 }
113 if _, _, _, entries, _ := c.stats(); entries != 0 {
114 t.Errorf("%d entries held, want 0", entries)
115 }
116}
117
118func TestZeroStaleKeepsNothingPastTTL(t *testing.T) {
119 up := &failingRT{}
120 up.set(200, "x", nil)
121 c := newAPICache(1<<20, time.Minute, 0)
122 now := time.Now()
123 c.now = func() time.Time { return now }
124 rt := c.transport(up)
125 get(t, rt, puppyURL)
126 now = now.Add(2 * time.Minute)
127 up.set(403, "blocked", nil)
128
129 if status, _ := get(t, rt, puppyURL); status != 403 {
130 t.Errorf("got %d with stale 0, want 403: nothing may be served past ttl", status)
131 }
132}