github.com/IRelaxxx/servefiles/v3@v3.4.6/assets_test.go (about) 1 // MIT License 2 // 3 // Copyright (c) 2016 Rick Beton 4 // 5 // Permission is hereby granted, free of charge, to any person obtaining a copy 6 // of this software and associated documentation files (the "Software"), to deal 7 // in the Software without restriction, including without limitation the rights 8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 // copies of the Software, and to permit persons to whom the Software is 10 // furnished to do so, subject to the following conditions: 11 // 12 // The above copyright notice and this permission notice shall be included in all 13 // copies or substantial portions of the Software. 14 // 15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 // SOFTWARE. 22 23 package servefiles 24 25 import ( 26 "fmt" 27 "log" 28 "net/http" 29 "net/http/httptest" 30 . "net/url" 31 "os" 32 "reflect" 33 "strings" 34 "testing" 35 "time" 36 37 "github.com/spf13/afero" 38 ) 39 40 var emptyStrings []string 41 42 func mustChdir(dir string) { 43 err := os.Chdir(dir) 44 if err != nil { 45 panic(err) 46 } 47 } 48 49 func init() { 50 mustChdir("test") 51 } 52 53 func TestChooseResourceSimpleNoGzip(t *testing.T) { 54 cases := []struct { 55 n int 56 maxAge time.Duration 57 url, path, cacheControl string 58 }{ 59 {0, 1, "http://localhost:8001/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=1"}, 60 {0, 3671, "http://localhost:8001/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=3671"}, 61 {3, 3671, "http://localhost:8001/x/y/z/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=3671"}, 62 } 63 64 for i, test := range cases { 65 etag := etagFor(test.path) 66 url := mustUrl(test.url) 67 request := &http.Request{Method: "GET", URL: url} 68 a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 69 w := httptest.NewRecorder() 70 71 a.ServeHTTP(w, request) 72 73 isEqual(t, w.Code, 200, i) 74 //isEqual(t, message, "", test.path) 75 isEqual(t, len(w.Header()["Expires"]), 1, i) 76 isGte(t, len(w.Header()["Expires"][0]), 25, i) 77 //fmt.Println(headers["Expires"]) 78 isEqual(t, w.Header()["Cache-Control"], []string{test.cacheControl}, i) 79 isEqual(t, w.Header()["Etag"], []string{etag}, i) 80 isEqual(t, w.Body.Len(), 160, i) 81 } 82 } 83 84 func TestChooseResourceSimpleNonExistent(t *testing.T) { 85 cases := []struct { 86 n int 87 maxAge time.Duration 88 url string 89 }{ 90 {0, time.Second, "http://localhost:8001/img/nonexisting.png"}, 91 {1, time.Second, "http://localhost:8001/a/img/nonexisting.png"}, 92 {2, time.Second, "http://localhost:8001/a/b/img/nonexisting.png"}, 93 } 94 95 for i, test := range cases { 96 url := mustUrl(test.url) 97 request := &http.Request{Method: "GET", URL: url} 98 a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge) 99 w := httptest.NewRecorder() 100 101 a.ServeHTTP(w, request) 102 103 isEqual(t, w.Code, 404, i) 104 //t.Logf("header %v", w.Header()) 105 isGte(t, len(w.Header()), 4, i) 106 isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i) 107 isEqual(t, w.Header().Get("Cache-Control"), "public, maxAge=1", i) 108 isGte(t, len(w.Header().Get("Expires")), 25, i) 109 } 110 } 111 112 func TestServeHTTP200WithGzipAndGzipWithAcceptHeader(t *testing.T) { 113 cases := []struct { 114 n int 115 maxAge time.Duration 116 url, mime, encoding, path, cacheControl string 117 }{ 118 {0, 1, "http://localhost:8001/css/style1.css", "text/css; charset=utf-8", "xx, gzip, zzz", "assets/css/style1.css.gz", "public, maxAge=1"}, 119 {2, 1, "http://localhost:8001/a/b/css/style1.css", "text/css; charset=utf-8", "xx, gzip, zzz", "assets/css/style1.css.gz", "public, maxAge=1"}, 120 {0, 1, "http://localhost:8001/js/script1.js", "text/javascript; charset=utf-8", "xx, gzip, zzz", "assets/js/script1.js.gz", "public, maxAge=1"}, 121 {2, 1, "http://localhost:8001/a/b/js/script1.js", "text/javascript; charset=utf-8", "xx, gzip, zzz", "assets/js/script1.js.gz", "public, maxAge=1"}, 122 } 123 124 for _, test := range cases { 125 etag := etagFor(test.path) 126 url := mustUrl(test.url) 127 header := newHeader("Accept-Encoding", test.encoding) 128 request := &http.Request{Method: "GET", URL: url, Header: header} 129 a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 130 w := httptest.NewRecorder() 131 132 a.ServeHTTP(w, request) 133 134 isEqual(t, w.Code, 200, test.path) 135 headers := w.Header() 136 //t.Logf("%+v\n", headers) 137 isGte(t, len(headers), 7, test.path) 138 isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path) 139 isEqual(t, headers["Content-Type"], []string{test.mime}, test.path) 140 isEqual(t, headers["X-Content-Type-Options"], []string{"nosniff"}, test.path) 141 isEqual(t, headers["Content-Encoding"], []string{"gzip"}, test.path) 142 isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, test.path) 143 isEqual(t, headers["Etag"], []string{"W/" + etag}, test.path) 144 isEqual(t, len(headers["Expires"]), 1, test.path) 145 isGte(t, len(headers["Expires"][0]), 25, test.path) 146 } 147 } 148 149 func TestServeHTTP200WithBrAndBrWithAcceptHeader(t *testing.T) { 150 cases := []struct { 151 n int 152 maxAge time.Duration 153 url, mime, encoding, path, cacheControl string 154 }{ 155 {0, 1, "http://localhost:8001/css/style1.css", "text/css; charset=utf-8", "br, gzip, zzz", "assets/css/style1.css.br", "public, maxAge=1"}, 156 {2, 1, "http://localhost:8001/a/b/css/style1.css", "text/css; charset=utf-8", "br, gzip, zzz", "assets/css/style1.css.br", "public, maxAge=1"}, 157 {0, 1, "http://localhost:8001/js/script1.js", "text/javascript; charset=utf-8", "br, gzip, zzz", "assets/js/script1.js.br", "public, maxAge=1"}, 158 {2, 1, "http://localhost:8001/a/b/js/script1.js", "text/javascript; charset=utf-8", "br, gzip, zzz", "assets/js/script1.js.br", "public, maxAge=1"}, 159 } 160 161 for _, test := range cases { 162 etag := etagFor(test.path) 163 url := mustUrl(test.url) 164 header := newHeader("Accept-Encoding", test.encoding) 165 request := &http.Request{Method: "GET", URL: url, Header: header} 166 a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 167 w := httptest.NewRecorder() 168 169 a.ServeHTTP(w, request) 170 171 isEqual(t, w.Code, 200, test.path) 172 headers := w.Header() 173 //t.Logf("%+v\n", headers) 174 isGte(t, len(headers), 7, test.path) 175 isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path) 176 isEqual(t, headers["Content-Type"], []string{test.mime}, test.path) 177 isEqual(t, headers["X-Content-Type-Options"], []string{"nosniff"}, test.path) 178 isEqual(t, headers["Content-Encoding"], []string{"br"}, test.path) 179 isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, test.path) 180 isEqual(t, headers["Etag"], []string{"W/" + etag}, test.path) 181 isEqual(t, len(headers["Expires"]), 1, test.path) 182 isGte(t, len(headers["Expires"][0]), 25, test.path) 183 } 184 } 185 186 func TestServeHTTP200WithGzipButNoAcceptHeader(t *testing.T) { 187 cases := []struct { 188 n int 189 maxAge time.Duration 190 url, mime, encoding, path, cacheControl string 191 }{ 192 {0, 1, "http://localhost:8001/css/style1.css", "text/css; charset=utf-8", "xx, yy, zzz", "assets/css/style1.css", "public, maxAge=1"}, 193 {2, 2, "http://localhost:8001/a/b/css/style1.css", "text/css; charset=utf-8", "xx, yy, zzz", "assets/css/style1.css", "public, maxAge=2"}, 194 {0, 3, "http://localhost:8001/js/script1.js", "text/javascript; charset=utf-8", "xx, yy, zzz", "assets/js/script1.js", "public, maxAge=3"}, 195 {2, 4, "http://localhost:8001/a/b/js/script1.js", "text/javascript; charset=utf-8", "xx, yy, zzz", "assets/js/script1.js", "public, maxAge=4"}, 196 } 197 198 for _, test := range cases { 199 etag := etagFor(test.path) 200 url := mustUrl(test.url) 201 header := newHeader("Accept-Encoding", test.encoding) 202 request := &http.Request{Method: "GET", URL: url, Header: header} 203 a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 204 w := httptest.NewRecorder() 205 206 a.ServeHTTP(w, request) 207 208 isEqual(t, w.Code, 200, test.path) 209 headers := w.Header() 210 //t.Logf("%+v\n", headers) 211 isGte(t, len(headers), 6, test.path) 212 isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path) 213 isEqual(t, headers["Content-Type"], []string{test.mime}, test.path) 214 isEqual(t, headers["Content-Encoding"], emptyStrings, test.path) 215 isEqual(t, headers["Vary"], emptyStrings, test.path) 216 isEqual(t, headers["Etag"], []string{etag}, test.path) 217 isEqual(t, len(headers["Expires"]), 1, test.path) 218 isGte(t, len(headers["Expires"][0]), 25, test.path) 219 } 220 } 221 222 func TestServeHTTP200WithGzipAcceptHeaderButNoGzippedFile(t *testing.T) { 223 cases := []struct { 224 n int 225 maxAge time.Duration 226 url, mime, encoding, path, cacheControl string 227 }{ 228 {0, 1, "http://localhost:8001/css/style2.css", "text/css; charset=utf-8", "xx, gzip, zzz", "assets/css/style2.css", "public, maxAge=1"}, 229 {0, 1, "http://localhost:8001/css/style2.css", "text/css; charset=utf-8", "br, gzip, zzz", "assets/css/style2.css", "public, maxAge=1"}, 230 {2, 2, "http://localhost:8001/a/b/css/style2.css", "text/css; charset=utf-8", "xx, gzip, zzz", "assets/css/style2.css", "public, maxAge=2"}, 231 {2, 2, "http://localhost:8001/a/b/css/style2.css", "text/css; charset=utf-8", "br, gzip, zzz", "assets/css/style2.css", "public, maxAge=2"}, 232 {0, 3, "http://localhost:8001/js/script2.js", "text/javascript; charset=utf-8", "xx, gzip, zzz", "assets/js/script2.js", "public, maxAge=3"}, 233 {0, 3, "http://localhost:8001/js/script2.js", "text/javascript; charset=utf-8", "br, gzip, zzz", "assets/js/script2.js", "public, maxAge=3"}, 234 {2, 4, "http://localhost:8001/a/b/js/script2.js", "text/javascript; charset=utf-8", "xx, gzip, zzz", "assets/js/script2.js", "public, maxAge=4"}, 235 {2, 4, "http://localhost:8001/a/b/js/script2.js", "text/javascript; charset=utf-8", "br, gzip, zzz", "assets/js/script2.js", "public, maxAge=4"}, 236 {0, 5, "http://localhost:8001/img/sort_asc.png", "image/png", "xx, gzip, zzz", "assets/img/sort_asc.png", "public, maxAge=5"}, 237 {0, 5, "http://localhost:8001/img/sort_asc.png", "image/png", "br, gzip, zzz", "assets/img/sort_asc.png", "public, maxAge=5"}, 238 {2, 6, "http://localhost:8001/a/b/img/sort_asc.png", "image/png", "xx, gzip, zzz", "assets/img/sort_asc.png", "public, maxAge=6"}, 239 {2, 6, "http://localhost:8001/a/b/img/sort_asc.png", "image/png", "br, gzip, zzz", "assets/img/sort_asc.png", "public, maxAge=6"}, 240 } 241 242 for _, test := range cases { 243 etag := etagFor(test.path) 244 url := mustUrl(test.url) 245 header := newHeader("Accept-Encoding", test.encoding) 246 request := &http.Request{Method: "GET", URL: url, Header: header} 247 a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 248 w := httptest.NewRecorder() 249 250 a.ServeHTTP(w, request) 251 252 isEqual(t, w.Code, 200, test.path) 253 headers := w.Header() 254 //t.Logf("%+v\n", headers) 255 isGte(t, len(headers), 6, test.path) 256 isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path) 257 isEqual(t, headers["Content-Type"], []string{test.mime}, test.path) 258 isEqual(t, headers["Content-Encoding"], emptyStrings, test.path) 259 isEqual(t, headers["Vary"], emptyStrings, test.path) 260 isEqual(t, headers["Etag"], []string{etag}, test.path) 261 isEqual(t, len(headers["Expires"]), 1, test.path) 262 isGte(t, len(headers["Expires"][0]), 25, test.path) 263 } 264 } 265 266 //------------------------------------------------------------------------------------------------- 267 268 type h404 struct{} 269 270 func (h *h404) ServeHTTP(w http.ResponseWriter, r *http.Request) { 271 w.Header().Set("Content-Type", "text/html") 272 w.WriteHeader(404) 273 w.Write([]byte("<html>foo</html>")) 274 } 275 276 func Test404Handler(t *testing.T) { 277 cases := []struct { 278 path, conType, response string 279 notFound http.Handler 280 }{ 281 {"/img/nonexisting.png", "text/plain; charset=utf-8", "404 Not found\n", nil}, 282 {"/img/nonexisting.png", "text/html", "<html>foo</html>", &h404{}}, 283 } 284 285 for i, test := range cases { 286 url := mustUrl("http://localhost:8001" + test.path) 287 request := &http.Request{Method: "GET", URL: url} 288 a := NewAssetHandler("./assets/").WithNotFound(test.notFound) 289 isEqual(t, a.NotFound, test.notFound, i) 290 w := httptest.NewRecorder() 291 292 a.ServeHTTP(w, request) 293 294 isEqual(t, w.Code, 404, i) 295 isEqual(t, w.Header().Get("Content-Type"), test.conType, i) 296 isEqual(t, w.Body.String(), test.response, i) 297 } 298 } 299 300 func Test403Handling(t *testing.T) { 301 cases := []struct { 302 path string 303 header http.Header 304 }{ 305 {"http://localhost:8001/css/style1.css", newHeader()}, 306 {"http://localhost:8001/css/style1.css", newHeader("Accept-Encoding", "gzip")}, 307 } 308 309 for i, test := range cases { 310 url := mustUrl("http://localhost:8001" + test.path) 311 request := &http.Request{Method: "GET", URL: url, Header: test.header} 312 a := NewAssetHandlerFS(&fs403{os.ErrPermission}) 313 w := httptest.NewRecorder() 314 315 a.ServeHTTP(w, request) 316 317 isEqual(t, w.Code, 403, i) 318 isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i) 319 isEqual(t, w.Body.String(), "403 Forbidden\n", i) 320 } 321 } 322 323 func Test503Handling(t *testing.T) { 324 cases := []struct { 325 path string 326 header http.Header 327 }{ 328 {"http://localhost:8001/css/style1.css", newHeader()}, 329 {"http://localhost:8001/css/style1.css", newHeader("Accept-Encoding", "gzip")}, 330 } 331 332 for i, test := range cases { 333 url := mustUrl("http://localhost:8001" + test.path) 334 request := &http.Request{Method: "GET", URL: url, Header: test.header} 335 a := NewAssetHandlerFS(&fs403{os.ErrInvalid}) 336 w := httptest.NewRecorder() 337 338 a.ServeHTTP(w, request) 339 340 isEqual(t, w.Code, 503, i) 341 isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i) 342 isNotEqual(t, w.Header().Get("Retry-After"), "", i) 343 isEqual(t, w.Body.String(), "503 Service unavailable\n", i) 344 } 345 } 346 347 //------------------------------------------------------------------------------------------------- 348 349 func TestServeHTTP304(t *testing.T) { 350 cases := []struct { 351 url, path, encoding string 352 notFound http.Handler 353 }{ 354 {"http://localhost:8001/css/style1.css", "assets/css/style1.css.gz", "gzip", nil}, 355 {"http://localhost:8001/css/style1.css", "assets/css/style1.css.br", "br", nil}, 356 {"http://localhost:8001/css/style2.css", "assets/css/style2.css", "xx", nil}, 357 {"http://localhost:8001/img/sort_asc.png", "assets/img/sort_asc.png", "xx", nil}, 358 {"http://localhost:8001/js/script1.js", "assets/js/script1.js.gz", "gzip", nil}, 359 {"http://localhost:8001/js/script1.js", "assets/js/script1.js.br", "br", nil}, 360 {"http://localhost:8001/js/script2.js", "assets/js/script2.js", "xx", nil}, 361 362 {"http://localhost:8001/css/style1.css", "assets/css/style1.css.gz", "gzip", &h404{}}, 363 {"http://localhost:8001/css/style1.css", "assets/css/style1.css.br", "br", &h404{}}, 364 {"http://localhost:8001/css/style2.css", "assets/css/style2.css", "xx", &h404{}}, 365 {"http://localhost:8001/img/sort_asc.png", "assets/img/sort_asc.png", "xx", &h404{}}, 366 {"http://localhost:8001/js/script1.js", "assets/js/script1.js.gz", "gzip", &h404{}}, 367 {"http://localhost:8001/js/script1.js", "assets/js/script1.js.br", "br", &h404{}}, 368 {"http://localhost:8001/js/script2.js", "assets/js/script2.js", "xx", &h404{}}, 369 } 370 371 // net/http serveFiles handles conditional requests according to RFC723x specs. 372 // So we only need to check that a conditional request is correctly wired in. 373 374 for i, test := range cases { 375 etag := etagFor(test.path) 376 url := mustUrl(test.url) 377 header := newHeader("Accept-Encoding", test.encoding, "If-None-Match", etag) 378 request := &http.Request{Method: "GET", URL: url, Header: header} 379 a := NewAssetHandler("./assets/").WithNotFound(test.notFound) 380 w := httptest.NewRecorder() 381 382 a.ServeHTTP(w, request) 383 384 isEqual(t, w.Code, 304, i) 385 headers := w.Header() 386 //t.Logf("%+v\n", headers) 387 isGte(t, len(headers), 1, i) 388 isEqual(t, headers["Cache-Control"], emptyStrings, i) 389 isEqual(t, headers["Content-Type"], emptyStrings, i) 390 isEqual(t, headers["Content-Length"], emptyStrings, i) 391 if strings.HasSuffix(test.path, ".gz") { 392 isEqual(t, headers["Content-Encoding"], []string{"gzip"}, i) 393 isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, i) 394 isEqual(t, headers["Etag"], []string{"W/" + etag}, i) 395 } else if strings.HasSuffix(test.path, ".br") { 396 isEqual(t, headers["Content-Encoding"], []string{"br"}, i) 397 isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, i) 398 isEqual(t, headers["Etag"], []string{"W/" + etag}, i) 399 } else { 400 isEqual(t, headers["Content-Encoding"], emptyStrings, i) 401 isEqual(t, headers["Vary"], emptyStrings, i) 402 isEqual(t, headers["Etag"], []string{etag}, i) 403 } 404 } 405 } 406 407 func TestSPA(t *testing.T) { 408 cases := []struct { 409 path, conType, response string 410 code int 411 }{ 412 {"/img/nonexisting.js", "text/plain; charset=utf-8", "404 Not found\n", 404}, 413 {"/img/nonexisting", "", "", 301}, 414 {"/img.de/nonexisting", "", "", 301}, 415 } 416 417 for i, test := range cases { 418 url := mustUrl("http://localhost:8001" + test.path) 419 request := &http.Request{Method: "GET", URL: url} 420 a := NewAssetHandler("./assets/").WithSPA() 421 isEqual(t, a.Spa, true, i) 422 w := httptest.NewRecorder() 423 424 a.ServeHTTP(w, request) 425 log.Printf(w.Body.String()) 426 isEqual(t, w.Code, test.code, i) 427 isEqual(t, w.Header().Get("Content-Type"), test.conType, i) 428 isEqual(t, w.Body.String(), test.response, i) 429 } 430 } 431 432 //------------------------------------------------------------------------------------------------- 433 434 func Benchmark(t *testing.B) { 435 t.StopTimer() 436 437 cases := []struct { 438 strip int 439 url, enc string 440 sendEtagFor string 441 code int 442 }{ 443 {0, "css/style1.css", "gzip", "", 200}, // has Gzip 444 {0, "css/style1.css", "br", "", 200}, // has Brotli 445 {1, "a/css/style1.css", "gzip", "", 200}, // has Gzip 446 {1, "a/css/style1.css", "br", "", 200}, // has Brotli 447 {2, "a/b/css/style1.css", "gzip", "", 200}, // has Gzip 448 {2, "a/b/css/style1.css", "br", "", 200}, // has Brotli 449 {2, "a/b/css/style1.css", "xxxx", "", 200}, // has Gzip 450 {2, "a/b/css/style1.css", "gzip", "assets/css/style1.css.gz", 304}, // has Gzip 451 {2, "a/b/css/style1.css", "br", "assets/css/style1.css.br", 304}, // has Brotli 452 {2, "a/b/css/style1.css", "xxxx", "assets/css/style1.css", 304}, // has Gzip 453 454 {2, "a/b/css/style2.css", "gzip", "", 200}, 455 {2, "a/b/css/style2.css", "xxxx", "", 200}, 456 {2, "a/b/css/style2.css", "gzip", "assets/css/style2.css", 304}, 457 {2, "a/a/css/style2.css", "xxxx", "assets/css/style2.css", 304}, 458 459 {2, "a/b/js/script1.js", "gzip", "", 200}, // has gzip 460 {2, "a/b/js/script1.js", "br", "", 200}, // has Brotli 461 {2, "a/b/js/script1.js", "xxxx", "", 200}, // has gzip 462 {2, "a/b/js/script1.js", "gzip", "assets/js/script1.js.gz", 304}, // has gzip 463 {2, "a/b/js/script1.js", "br", "assets/js/script1.js.br", 304}, // has Brotli 464 {2, "a/a/js/script1.js", "xxxx", "assets/js/script1.js", 304}, // has gzip 465 466 {2, "a/b/js/script2.js", "gzip", "", 200}, 467 {2, "a/b/js/script2.js", "xxxx", "", 200}, 468 {2, "a/b/js/script2.js", "gzip", "assets/js/script2.js", 304}, 469 {2, "a/a/js/script2.js", "xxxx", "assets/js/script2.js", 304}, 470 471 {2, "a/b/img/sort_asc.png", "gzip", "", 200}, 472 {2, "a/b/img/sort_asc.png", "xxxx", "", 200}, 473 {2, "a/b/img/sort_asc.png", "gzip", "assets/img/sort_asc.png", 304}, 474 {2, "a/a/img/sort_asc.png", "xxxx", "assets/img/sort_asc.png", 304}, 475 476 {2, "a/b/img/nonexisting.png", "gzip", "", 404}, 477 {2, "a/b/img/nonexisting.png", "xxxx", "", 404}, 478 } 479 480 ages := []time.Duration{0, time.Hour} 481 482 for _, test := range cases { 483 header := newHeader("Accept-Encoding", test.enc) 484 etagOn := "no-etag" 485 if test.sendEtagFor != "" { 486 header = newHeader("Accept-Encoding", test.enc, "If-None-Match", etagFor(test.sendEtagFor)) 487 etagOn = "etag" 488 } 489 490 for _, age := range ages { 491 a := NewAssetHandler("./assets/").StripOff(test.strip).WithMaxAge(age) 492 493 t.Run(fmt.Sprintf("%s~%s~%v~%d~%v", test.url, test.enc, etagOn, test.code, age), func(b *testing.B) { 494 b.StopTimer() 495 496 for i := 0; i < b.N; i++ { 497 url := mustUrl("http://localhost:8001/" + test.url) 498 request := &http.Request{Method: "GET", URL: url, Header: header} 499 w := httptest.NewRecorder() 500 501 b.StartTimer() 502 a.ServeHTTP(w, request) 503 b.StopTimer() 504 505 if w.Code != test.code { 506 b.Fatalf("Expected %d but got %d", test.code, w.Code) 507 } 508 } 509 }) 510 } 511 } 512 } 513 514 //------------------------------------------------------------------------------------------------- 515 516 func isEqual(t *testing.T, a, b, hint interface{}) { 517 t.Helper() 518 if !reflect.DeepEqual(a, b) { 519 t.Errorf("Got %#v; expected %#v - for %v\n", a, b, hint) 520 } 521 } 522 523 func isNotEqual(t *testing.T, a, b, hint interface{}) { 524 t.Helper() 525 if reflect.DeepEqual(a, b) { 526 t.Errorf("Got %#v; expected something else - for %v\n", a, hint) 527 } 528 } 529 530 func isGte(t *testing.T, a, b int, hint interface{}) { 531 t.Helper() 532 if a < b { 533 t.Errorf("Got %d; expected at least %d - for %v\n", a, b, hint) 534 } 535 } 536 537 func mustUrl(s string) *URL { 538 parsed, err := Parse(s) 539 must(err) 540 return parsed 541 } 542 543 func newHeader(kv ...string) http.Header { 544 header := make(http.Header) 545 for i, x := range kv { 546 if i%2 == 0 { 547 header[x] = []string{kv[i+1]} 548 } 549 } 550 return header 551 } 552 553 // must abort the program on error, printing a stack trace. 554 func must(err error) { 555 if err != nil { 556 panic(err) 557 } 558 } 559 560 func mustStat(name string) os.FileInfo { 561 d, err := os.Stat(name) 562 if err != nil { 563 panic(err) 564 } 565 return d 566 } 567 568 func etagFor(name string) string { 569 d := mustStat(name) 570 t := "" 571 return fmt.Sprintf(`%s"%x-%x"`, t, d.ModTime().Unix(), d.Size()) 572 } 573 574 //------------------------------------------------------------------------------------------------- 575 576 type fs403 struct { 577 err error 578 } 579 580 func (fs fs403) Create(name string) (afero.File, error) { 581 return nil, fs.err 582 } 583 584 func (fs fs403) Mkdir(name string, perm os.FileMode) error { 585 return fs.err 586 } 587 588 func (fs fs403) MkdirAll(path string, perm os.FileMode) error { 589 return fs.err 590 } 591 592 func (fs fs403) Open(name string) (afero.File, error) { 593 return nil, fs.err 594 } 595 596 func (fs fs403) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { 597 return nil, fs.err 598 } 599 600 func (fs fs403) Remove(name string) error { 601 return fs.err 602 } 603 604 func (fs fs403) RemoveAll(path string) error { 605 return fs.err 606 } 607 608 func (fs fs403) Rename(oldname, newname string) error { 609 return fs.err 610 } 611 612 func (fs fs403) Stat(name string) (os.FileInfo, error) { 613 return nil, fs.err 614 } 615 616 func (fs403) Name() string { 617 return "dumb" 618 } 619 620 func (fs fs403) Chmod(name string, mode os.FileMode) error { 621 return fs.err 622 } 623 624 func (fs fs403) Chtimes(name string, atime time.Time, mtime time.Time) error { 625 return fs.err 626 }