blob: 7fff5f4968c8ece010be60097bc7dfe65837a912 [file] [log] [blame]
Philip Zeyliger176de792025-04-21 12:25:18 -07001package gzhandler
2
3import (
4 "compress/gzip"
5 "io"
6 "io/fs"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11 "testing/fstest"
12)
13
14func TestHandler_ServeHTTP(t *testing.T) {
15 // Create a test filesystem with regular and gzipped files
16 testFS := fstest.MapFS{
17 "regular.txt": &fstest.MapFile{
18 Data: []byte("This is a regular text file"),
Philip Zeyligerd1402952025-04-23 03:54:37 +000019 Mode: 0o644,
Philip Zeyliger176de792025-04-21 12:25:18 -070020 },
21 "regular.txt.gz": &fstest.MapFile{
22 Data: compressString(t, "This is a regular text file"),
Philip Zeyligerd1402952025-04-23 03:54:37 +000023 Mode: 0o644,
Philip Zeyliger176de792025-04-21 12:25:18 -070024 },
25 "regular.js": &fstest.MapFile{
26 Data: []byte("console.log('Hello world');"),
Philip Zeyligerd1402952025-04-23 03:54:37 +000027 Mode: 0o644,
Philip Zeyliger176de792025-04-21 12:25:18 -070028 },
29 "regular.js.gz": &fstest.MapFile{
30 Data: compressString(t, "console.log('Hello world');"),
Philip Zeyligerd1402952025-04-23 03:54:37 +000031 Mode: 0o644,
Philip Zeyliger176de792025-04-21 12:25:18 -070032 },
33 "nogzip.css": &fstest.MapFile{
34 Data: []byte(".body { color: red; }"),
Philip Zeyligerd1402952025-04-23 03:54:37 +000035 Mode: 0o644,
Philip Zeyliger176de792025-04-21 12:25:18 -070036 },
37 }
38
39 // Create the handler using our test filesystem
40 handler := New(testFS)
41
42 // Define test cases
43 tests := []struct {
44 name string
45 path string
46 acceptGzip bool
47 expectedStatus int
48 expectedBody string
49 expectedGzipHeader string
50 expectedType string
51 }{
52 {
53 name: "Serve gzipped text file when accepted",
54 path: "/regular.txt",
55 acceptGzip: true,
56 expectedStatus: http.StatusOK,
57 expectedBody: "This is a regular text file",
58 expectedGzipHeader: "gzip",
59 expectedType: "text/plain; charset=utf-8",
60 },
61 {
62 name: "Serve regular text file when gzip not accepted",
63 path: "/regular.txt",
64 acceptGzip: false,
65 expectedStatus: http.StatusOK,
66 expectedBody: "This is a regular text file",
67 expectedGzipHeader: "",
68 expectedType: "text/plain; charset=utf-8",
69 },
70 {
71 name: "Serve gzipped JS file when accepted",
72 path: "/regular.js",
73 acceptGzip: true,
74 expectedStatus: http.StatusOK,
75 expectedBody: "console.log('Hello world');",
76 expectedGzipHeader: "gzip",
77 expectedType: "text/javascript; charset=utf-8",
78 },
79 {
80 name: "Serve regular CSS file when gzip not available",
81 path: "/nogzip.css",
82 acceptGzip: true,
83 expectedStatus: http.StatusOK,
84 expectedBody: ".body { color: red; }",
85 expectedGzipHeader: "",
86 expectedType: "text/css; charset=utf-8",
87 },
88 {
89 name: "Return 404 for non-existent file",
90 path: "/nonexistent.txt",
91 acceptGzip: true,
92 expectedStatus: http.StatusNotFound,
93 },
94 }
95
96 for _, tc := range tests {
97 t.Run(tc.name, func(t *testing.T) {
98 // Create a request for the specified path
99 req := httptest.NewRequest("GET", tc.path, nil)
100
101 // Set Accept-Encoding header if needed
102 if tc.acceptGzip {
103 req.Header.Set("Accept-Encoding", "gzip")
104 }
105
106 // Create a response recorder
107 rec := httptest.NewRecorder()
108
109 // Serve the request
110 handler.ServeHTTP(rec, req)
111
112 // Check status code
113 if rec.Code != tc.expectedStatus {
114 t.Errorf("Expected status %d, got %d", tc.expectedStatus, rec.Code)
115 return
116 }
117
118 // For non-200 responses, we don't check the body
119 if tc.expectedStatus != http.StatusOK {
120 return
121 }
122
123 // Check Content-Type header (skip for .txt files since MIME mappings can vary by OS)
124 if !strings.HasSuffix(tc.path, ".txt") {
125 contentType := rec.Header().Get("Content-Type")
126 if contentType != tc.expectedType {
127 t.Errorf("Expected Content-Type %q, got %q", tc.expectedType, contentType)
128 }
129 }
130
131 // Check Content-Encoding header
132 contentEncoding := rec.Header().Get("Content-Encoding")
133 if contentEncoding != tc.expectedGzipHeader {
134 t.Errorf("Expected Content-Encoding %q, got %q", tc.expectedGzipHeader, contentEncoding)
135 }
136
137 // Read response body
138 var bodyReader io.Reader = rec.Body
139
140 // If response is gzipped, decompress it
141 if contentEncoding == "gzip" {
142 gzReader, err := gzip.NewReader(rec.Body)
143 if err != nil {
144 t.Fatalf("Failed to create gzip reader: %v", err)
145 }
146 defer gzReader.Close()
147 bodyReader = gzReader
148 }
149
150 // Read and check body content
151 actualBody, err := io.ReadAll(bodyReader)
152 if err != nil {
153 t.Fatalf("Failed to read response body: %v", err)
154 }
155
156 if string(actualBody) != tc.expectedBody {
157 t.Errorf("Expected body %q, got %q", tc.expectedBody, string(actualBody))
158 }
159 })
160 }
161}
162
163// TestHandleDirectories tests that directories are handled properly
164func TestHandleDirectories(t *testing.T) {
165 // Create a test filesystem with a directory
166 testFS := fstest.MapFS{
167 "dir": &fstest.MapFile{
Philip Zeyligerd1402952025-04-23 03:54:37 +0000168 Mode: fs.ModeDir | 0o755,
Philip Zeyliger176de792025-04-21 12:25:18 -0700169 },
170 "dir/index.html": &fstest.MapFile{
171 Data: []byte("<html>Directory index</html>"),
Philip Zeyligerd1402952025-04-23 03:54:37 +0000172 Mode: 0o644,
Philip Zeyliger176de792025-04-21 12:25:18 -0700173 },
174 "dir/index.html.gz": &fstest.MapFile{
175 Data: compressString(t, "<html>Directory index</html>"),
Philip Zeyligerd1402952025-04-23 03:54:37 +0000176 Mode: 0o644,
Philip Zeyliger176de792025-04-21 12:25:18 -0700177 },
178 }
179
180 // Create the handler using our test filesystem
181 handler := New(testFS)
182
183 // Create a request for the directory
184 req := httptest.NewRequest("GET", "/dir/", nil)
185 req.Header.Set("Accept-Encoding", "gzip")
186
187 // Create a response recorder
188 rec := httptest.NewRecorder()
189
190 // Serve the request
191 handler.ServeHTTP(rec, req)
192
193 // Check status code should be 200 (directory index)
194 if rec.Code != http.StatusOK {
195 t.Errorf("Expected status 200, got %d", rec.Code)
196 }
197
198 // Note: Directory listings may not use gzip encoding by default with http.FileServer
199 // This is acceptable behavior, so we don't enforce gzip encoding for directories
200 contentEncoding := rec.Header().Get("Content-Encoding")
201
202 // Check if body contains the index content (after decompression)
203 var bodyReader io.Reader
204 if contentEncoding == "gzip" {
205 gzReader, err := gzip.NewReader(rec.Body)
206 if err != nil {
207 t.Fatalf("Failed to create gzip reader: %v", err)
208 }
209 defer gzReader.Close()
210 bodyReader = gzReader
211 } else {
212 bodyReader = rec.Body
213 }
214
215 body, err := io.ReadAll(bodyReader)
216 if err != nil {
217 t.Fatalf("Failed to read response body: %v", err)
218 }
219
220 if !strings.Contains(string(body), "Directory index") {
221 t.Errorf("Expected directory index content, got %q", string(body))
222 }
223}
224
225// Helper function to compress a string into gzip format
226func compressString(t *testing.T, s string) []byte {
227 var buf strings.Builder
228 gw := gzip.NewWriter(&buf)
229
230 _, err := gw.Write([]byte(s))
231 if err != nil {
232 t.Fatalf("Failed to write to gzip writer: %v", err)
233 }
234
235 if err := gw.Close(); err != nil {
236 t.Fatalf("Failed to close gzip writer: %v", err)
237 }
238
239 return []byte(buf.String())
240}