app/apicache_stale_test.go

v1.5.4
skunky-art/app/apicache_stale_test.go history · blame · raw

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}