Skip to content

Commit 03dc2ae

Browse files
committed
More tests and documentation.
1 parent e8f70b0 commit 03dc2ae

File tree

6 files changed

+817
-8
lines changed

6 files changed

+817
-8
lines changed

pkg/middleware/cache.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ import (
55
"strings"
66
)
77

8+
// CacheMiddleware sets Cache-Control headers for specified file suffixes.
89
func CacheMiddleware(suffixes ...string) MiddlewareFunc {
910
return func(handler http.Handler) http.Handler {
1011
return Cache(handler, suffixes...)
1112
}
1213
}
1314

15+
// Cache wraps an http.Handler to set Cache-Control headers for specified
16+
// file suffixes.
17+
//
18+
// e.g., Cache(handler, "js", "css") will set
19+
// "Cache-Control: max-age=31536000, public" for requests ending with .js or .css
1420
func Cache(handler http.Handler, suffixes ...string) http.Handler {
1521
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1622
i := strings.LastIndex(r.URL.Path, ".")
@@ -25,20 +31,23 @@ func Cache(handler http.Handler, suffixes ...string) http.Handler {
2531
})
2632
}
2733

34+
// NoCache sets no-cache headers on responses.
2835
func NoCache(handler http.Handler) http.Handler {
2936
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3037
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
3138
handler.ServeHTTP(w, r)
3239
})
3340
}
3441

42+
// FrameOptions middleware sets X-Frame-Options header on responses.
3543
func FrameOptions(handler http.Handler) http.Handler {
3644
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3745
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
3846
handler.ServeHTTP(w, r)
3947
})
4048
}
4149

50+
// ContentTypeOptions middleware sets X-Content-Type-Options header on responses.
4251
func ContentTypeOptions(handler http.Handler) http.Handler {
4352
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
4453
w.Header().Set("X-Content-Type-Options", "nosniff")

pkg/middleware/cache_test.go

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// TestCacheMiddleware tests the CacheMiddleware function.
12+
//
13+
// It verifies that the middleware sets the correct Cache-Control header for
14+
// requests with specified file suffixes.
15+
func TestCacheMiddleware(t *testing.T) {
16+
assert := assert.New(t)
17+
18+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19+
w.WriteHeader(http.StatusOK)
20+
})
21+
22+
middleware := CacheMiddleware("js", "css")
23+
wrappedHandler := middleware(handler)
24+
25+
// Test with .js file
26+
req := httptest.NewRequest("GET", "/app.js", nil)
27+
w := httptest.NewRecorder()
28+
wrappedHandler.ServeHTTP(w, req)
29+
30+
assert.Equal("max-age=31536000, public", w.Header().Get("Cache-Control"))
31+
assert.Equal(http.StatusOK, w.Code)
32+
}
33+
34+
// TestCacheMatchingSuffix tests that Cache sets headers for matching file suffixes
35+
func TestCacheMatchingSuffix(t *testing.T) {
36+
assert := assert.New(t)
37+
38+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
39+
w.WriteHeader(http.StatusOK)
40+
})
41+
42+
wrappedHandler := Cache(handler, "js", "css", "png")
43+
44+
tests := []struct {
45+
name string
46+
path string
47+
}{
48+
{"JavaScript file", "/static/app.js"},
49+
{"CSS file", "/styles/main.css"},
50+
{"PNG image", "/images/logo.png"},
51+
{"Nested path with JS", "/vendor/lib/script.js"},
52+
{"Path with multiple dots", "/file.min.js"},
53+
}
54+
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) {
57+
req := httptest.NewRequest("GET", tt.path, nil)
58+
w := httptest.NewRecorder()
59+
wrappedHandler.ServeHTTP(w, req)
60+
61+
assert.Equal("max-age=31536000, public", w.Header().Get("Cache-Control"),
62+
"Cache-Control should be set for %s", tt.path)
63+
assert.Equal(http.StatusOK, w.Code)
64+
})
65+
}
66+
}
67+
68+
// TestCacheNonMatchingSuffix tests that Cache does not set headers for non-matching suffixes
69+
func TestCacheNonMatchingSuffix(t *testing.T) {
70+
assert := assert.New(t)
71+
72+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
73+
w.WriteHeader(http.StatusOK)
74+
})
75+
76+
wrappedHandler := Cache(handler, "js", "css")
77+
78+
tests := []struct {
79+
name string
80+
path string
81+
}{
82+
{"HTML file", "/index.html"},
83+
{"JSON file", "/api/data.json"},
84+
{"No extension", "/path/without/extension"},
85+
{"Different extension", "/document.pdf"},
86+
{"Root path", "/"},
87+
}
88+
89+
for _, tt := range tests {
90+
t.Run(tt.name, func(t *testing.T) {
91+
req := httptest.NewRequest("GET", tt.path, nil)
92+
w := httptest.NewRecorder()
93+
wrappedHandler.ServeHTTP(w, req)
94+
95+
assert.Equal("", w.Header().Get("Cache-Control"),
96+
"Cache-Control should not be set for %s", tt.path)
97+
assert.Equal(http.StatusOK, w.Code)
98+
})
99+
}
100+
}
101+
102+
// TestCacheEmptySuffixes tests Cache with no suffixes provided
103+
func TestCacheEmptySuffixes(t *testing.T) {
104+
assert := assert.New(t)
105+
106+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
107+
w.WriteHeader(http.StatusOK)
108+
})
109+
110+
wrappedHandler := Cache(handler)
111+
112+
req := httptest.NewRequest("GET", "/app.js", nil)
113+
w := httptest.NewRecorder()
114+
wrappedHandler.ServeHTTP(w, req)
115+
116+
assert.Equal("", w.Header().Get("Cache-Control"),
117+
"Cache-Control should not be set when no suffixes are provided")
118+
assert.Equal(http.StatusOK, w.Code)
119+
}
120+
121+
// TestCacheCaseSensitive tests that suffix matching is case-sensitive
122+
func TestCacheCaseSensitive(t *testing.T) {
123+
assert := assert.New(t)
124+
125+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
126+
w.WriteHeader(http.StatusOK)
127+
})
128+
129+
wrappedHandler := Cache(handler, "js")
130+
131+
// Test lowercase (should match)
132+
req := httptest.NewRequest("GET", "/app.js", nil)
133+
w := httptest.NewRecorder()
134+
wrappedHandler.ServeHTTP(w, req)
135+
assert.Equal("max-age=31536000, public", w.Header().Get("Cache-Control"))
136+
137+
// Test uppercase (should not match)
138+
req = httptest.NewRequest("GET", "/app.JS", nil)
139+
w = httptest.NewRecorder()
140+
wrappedHandler.ServeHTTP(w, req)
141+
assert.Equal("", w.Header().Get("Cache-Control"),
142+
"Suffix matching should be case-sensitive")
143+
}
144+
145+
// TestCacheHandlerStillCalled tests that the wrapped handler is always called
146+
func TestCacheHandlerStillCalled(t *testing.T) {
147+
assert := assert.New(t)
148+
149+
handlerCalled := false
150+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151+
handlerCalled = true
152+
w.WriteHeader(http.StatusOK)
153+
})
154+
155+
wrappedHandler := Cache(handler, "js")
156+
157+
req := httptest.NewRequest("GET", "/app.js", nil)
158+
w := httptest.NewRecorder()
159+
wrappedHandler.ServeHTTP(w, req)
160+
161+
assert.True(handlerCalled, "Wrapped handler should be called")
162+
}
163+
164+
// TestNoCache tests the NoCache middleware
165+
func TestNoCache(t *testing.T) {
166+
assert := assert.New(t)
167+
168+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
169+
w.WriteHeader(http.StatusOK)
170+
w.Write([]byte("response"))
171+
})
172+
173+
wrappedHandler := NoCache(handler)
174+
175+
tests := []struct {
176+
name string
177+
path string
178+
}{
179+
{"Root path", "/"},
180+
{"API endpoint", "/api/data"},
181+
{"With extension", "/file.html"},
182+
{"Nested path", "/a/b/c/d"},
183+
}
184+
185+
for _, tt := range tests {
186+
t.Run(tt.name, func(t *testing.T) {
187+
req := httptest.NewRequest("GET", tt.path, nil)
188+
w := httptest.NewRecorder()
189+
wrappedHandler.ServeHTTP(w, req)
190+
191+
assert.Equal("no-cache, no-store, must-revalidate", w.Header().Get("Cache-Control"),
192+
"NoCache should set Cache-Control header for %s", tt.path)
193+
assert.Equal(http.StatusOK, w.Code)
194+
assert.Equal("response", w.Body.String())
195+
})
196+
}
197+
}
198+
199+
// TestFrameOptions tests the FrameOptions middleware
200+
func TestFrameOptions(t *testing.T) {
201+
assert := assert.New(t)
202+
203+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
204+
w.WriteHeader(http.StatusOK)
205+
w.Write([]byte("response"))
206+
})
207+
208+
wrappedHandler := FrameOptions(handler)
209+
210+
tests := []struct {
211+
name string
212+
path string
213+
}{
214+
{"Root path", "/"},
215+
{"HTML page", "/index.html"},
216+
{"Nested path", "/admin/dashboard"},
217+
}
218+
219+
for _, tt := range tests {
220+
t.Run(tt.name, func(t *testing.T) {
221+
req := httptest.NewRequest("GET", tt.path, nil)
222+
w := httptest.NewRecorder()
223+
wrappedHandler.ServeHTTP(w, req)
224+
225+
assert.Equal("SAMEORIGIN", w.Header().Get("X-Frame-Options"),
226+
"FrameOptions should set X-Frame-Options header for %s", tt.path)
227+
assert.Equal(http.StatusOK, w.Code)
228+
assert.Equal("response", w.Body.String())
229+
})
230+
}
231+
}
232+
233+
// TestContentTypeOptions tests the ContentTypeOptions middleware
234+
func TestContentTypeOptions(t *testing.T) {
235+
assert := assert.New(t)
236+
237+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
238+
w.WriteHeader(http.StatusOK)
239+
w.Write([]byte("response"))
240+
})
241+
242+
wrappedHandler := ContentTypeOptions(handler)
243+
244+
tests := []struct {
245+
name string
246+
path string
247+
}{
248+
{"Root path", "/"},
249+
{"JavaScript file", "/app.js"},
250+
{"API endpoint", "/api/users"},
251+
}
252+
253+
for _, tt := range tests {
254+
t.Run(tt.name, func(t *testing.T) {
255+
req := httptest.NewRequest("GET", tt.path, nil)
256+
w := httptest.NewRecorder()
257+
wrappedHandler.ServeHTTP(w, req)
258+
259+
assert.Equal("nosniff", w.Header().Get("X-Content-Type-Options"),
260+
"ContentTypeOptions should set X-Content-Type-Options header for %s", tt.path)
261+
assert.Equal(http.StatusOK, w.Code)
262+
assert.Equal("response", w.Body.String())
263+
})
264+
}
265+
}
266+
267+
// TestMiddlewareCombination tests combining multiple middleware functions
268+
func TestMiddlewareCombination(t *testing.T) {
269+
assert := assert.New(t)
270+
271+
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
272+
w.WriteHeader(http.StatusOK)
273+
w.Write([]byte("response"))
274+
})
275+
276+
// Combine multiple middleware
277+
wrappedHandler := NoCache(FrameOptions(ContentTypeOptions(handler)))
278+
279+
req := httptest.NewRequest("GET", "/secure/page", nil)
280+
w := httptest.NewRecorder()
281+
wrappedHandler.ServeHTTP(w, req)
282+
283+
assert.Equal("no-cache, no-store, must-revalidate", w.Header().Get("Cache-Control"))
284+
assert.Equal("SAMEORIGIN", w.Header().Get("X-Frame-Options"))
285+
assert.Equal("nosniff", w.Header().Get("X-Content-Type-Options"))
286+
assert.Equal(http.StatusOK, w.Code)
287+
assert.Equal("response", w.Body.String())
288+
}

pkg/middleware/content.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,24 @@ import (
66
"net"
77
"net/http"
88
"reflect"
9-
"strings"
109
)
1110

11+
// ContentTypeWriter is a custom ResponseWriter that sets the Content-Type
12+
// header if it is not already set.
1213
type ContentTypeWriter struct {
1314
http.ResponseWriter
1415
}
1516

17+
// Write sets the Content-Type header if not already set, then writes the response.
1618
func (c ContentTypeWriter) Write(b []byte) (int, error) {
17-
found := false
18-
for k := range c.Header() {
19-
if strings.EqualFold(k, "Content-Type") {
20-
found = true
21-
break
22-
}
23-
}
19+
found := c.Header().Get("Content-Type") != ""
2420
if !found {
2521
c.Header().Set("Content-Type", http.DetectContentType(b))
2622
}
2723
return c.ResponseWriter.Write(b)
2824
}
2925

26+
// ContentType is a middleware that sets the Content-Type header if it is not already set.
3027
func ContentType(handler http.Handler) http.Handler {
3128
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
3229
writer := ContentTypeWriter{ResponseWriter: w}

0 commit comments

Comments
 (0)