-
Notifications
You must be signed in to change notification settings - Fork 70
Expand file tree
/
Copy pathcollision_test.go
More file actions
139 lines (125 loc) · 4.03 KB
/
Copy pathcollision_test.go
File metadata and controls
139 lines (125 loc) · 4.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package cache
import (
"bytes"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
// fixedKeyAdapter ignores the supplied key and stores a single entry,
// simulating a worst-case 100% hash collision: every request reads the
// same blob unless the middleware can detect the collision some other
// way.
type fixedKeyAdapter struct {
mu sync.Mutex
blob []byte
}
func (a *fixedKeyAdapter) Get(uint64) ([]byte, bool) {
a.mu.Lock()
defer a.mu.Unlock()
if a.blob == nil {
return nil, false
}
return a.blob, true
}
func (a *fixedKeyAdapter) Set(_ uint64, b []byte, _ time.Time) {
a.mu.Lock()
defer a.mu.Unlock()
a.blob = append(a.blob[:0], b...)
}
func (a *fixedKeyAdapter) Release(uint64) {
a.mu.Lock()
defer a.mu.Unlock()
a.blob = nil
}
// FNV-64 collisions are rare but inevitable at scale. When two distinct
// requests hash to the same key, the middleware must not serve one
// user's cached response to the other. Verification of the stored
// canonical key fingerprint catches this.
func TestMiddlewareDetectsHashCollisionViaCanonicalKey(t *testing.T) {
adapter := &fixedKeyAdapter{}
client, err := NewClient(
ClientWithAdapter(adapter),
ClientWithTTL(1*time.Minute),
)
if err != nil {
t.Fatal(err)
}
handler := client.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("response for " + r.URL.Path))
}))
// Warm the cache for /user-a.
handler.ServeHTTP(httptest.NewRecorder(),
httptest.NewRequest(http.MethodGet, "http://x/user-a", nil))
// Request /user-b: distinct URL, so canonical fingerprint differs.
// With the fixed-key adapter, Get returns /user-a's blob; the
// middleware must detect the mismatch and serve a fresh response.
w := httptest.NewRecorder()
handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "http://x/user-b", nil))
if got, want := w.Body.String(), "response for /user-b"; got != want {
t.Errorf("cross-user leak: got %q, want %q", got, want)
}
}
// Pre-upgrade entries written without a canonical fingerprint must
// continue to be served (backward compatibility for entries already
// living in Redis or other persistent adapters).
func TestMiddlewareServesLegacyEntriesWithoutCanonicalKey(t *testing.T) {
const url = "http://x/legacy"
legacy := Response{
Value: []byte("legacy body"),
Expiration: time.Now().Add(1 * time.Minute),
}.Bytes()
adapter := &adapterMock{store: map[uint64][]byte{
generateKey(url): legacy,
}}
client, err := NewClient(
ClientWithAdapter(adapter),
ClientWithTTL(1*time.Minute),
)
if err != nil {
t.Fatal(err)
}
handler := client.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("fresh"))
}))
w := httptest.NewRecorder()
handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, url, nil))
if got, want := w.Body.String(), "legacy body"; got != want {
t.Errorf("legacy entry not served: got %q, want %q", got, want)
}
}
// New writes must include a canonical fingerprint so future collisions
// can be detected.
func TestMiddlewareWritesCanonicalKeyOnStore(t *testing.T) {
adapter := &adapterMock{store: map[uint64][]byte{}}
client, err := NewClient(
ClientWithAdapter(adapter),
ClientWithTTL(1*time.Minute),
)
if err != nil {
t.Fatal(err)
}
handler := client.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("body"))
}))
const url = "http://x/canon"
handler.ServeHTTP(httptest.NewRecorder(),
httptest.NewRequest(http.MethodGet, url, nil))
stored, ok := adapter.Get(generateKey(url))
if !ok {
t.Fatal("response was not cached")
}
r := BytesToResponse(stored)
if len(r.CanonicalKey) == 0 {
t.Fatal("CanonicalKey was not written on store")
}
// The fingerprint should be deterministic.
handler.ServeHTTP(httptest.NewRecorder(),
httptest.NewRequest(http.MethodGet, url, nil))
stored2, _ := adapter.Get(generateKey(url))
r2 := BytesToResponse(stored2)
if !bytes.Equal(r.CanonicalKey, r2.CanonicalKey) {
t.Errorf("canonical fingerprint is not deterministic: %x vs %x", r.CanonicalKey, r2.CanonicalKey)
}
}