github.com/pzeinlinger/servefiles/v3@v3.6.0/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("testdata") 51 } 52 53 const ( 54 cssMimeType = "text/css; charset=utf-8" 55 javascriptMimeType = "text/javascript; charset=utf-8" 56 ) 57 58 func TestChooseResourceSimpleDirNoGzip(t *testing.T) { 59 cases := []struct { 60 n int 61 maxAge time.Duration 62 url, path, cacheControl string 63 }{ 64 {0, 1, "/", "assets/index.html", "public, max-age=1"}, 65 } 66 67 for i, test := range cases { 68 etag := etagFor(test.path) 69 url := mustUrl(test.url) 70 request := &http.Request{Method: "GET", URL: url} 71 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 72 w := httptest.NewRecorder() 73 74 a.ServeHTTP(w, request) 75 76 isEqual(t, w.Code, 200, i) 77 //isEqual(t, message, "", test.path) 78 isEqual(t, w.Header()["Cache-Control"], []string{test.cacheControl}, i) 79 isEqual(t, w.Header()["Etag"], []string{etag}, i) 80 } 81 } 82 83 func TestChooseResourceSimpleNoGzip(t *testing.T) { 84 cases := []struct { 85 n int 86 maxAge time.Duration 87 url, path, cacheControl string 88 }{ 89 {0, 1, "/img/sort_asc.png", "assets/img/sort_asc.png", "public, max-age=1"}, 90 {0, 3671, "/img/sort_asc.png", "assets/img/sort_asc.png", "public, max-age=3671"}, 91 {3, 3671, "/x/y/z/img/sort_asc.png", "assets/img/sort_asc.png", "public, max-age=3671"}, 92 } 93 94 for i, test := range cases { 95 etag := etagFor(test.path) 96 url := mustUrl(test.url) 97 request := &http.Request{Method: "GET", URL: url} 98 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 99 w := httptest.NewRecorder() 100 101 a.ServeHTTP(w, request) 102 103 isEqual(t, w.Code, 200, i) 104 //isEqual(t, message, "", test.path) 105 isEqual(t, w.Header()["Cache-Control"], []string{test.cacheControl}, i) 106 isEqual(t, w.Header()["Etag"], []string{etag}, i) 107 isEqual(t, w.Body.Len(), 160, i) 108 } 109 } 110 111 func TestChooseResourceSimpleNonExistent(t *testing.T) { 112 cases := []struct { 113 n int 114 maxAge time.Duration 115 url string 116 }{ 117 {0, time.Second, "/img/nonexisting.png"}, 118 {1, time.Second, "/a/img/nonexisting.png"}, 119 {2, time.Second, "/a/b/img/nonexisting.png"}, 120 } 121 122 for i, test := range cases { 123 url := mustUrl(test.url) 124 request := &http.Request{Method: "GET", URL: url} 125 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).StripOff(test.n).WithMaxAge(test.maxAge) 126 w := httptest.NewRecorder() 127 128 a.ServeHTTP(w, request) 129 130 isEqual(t, w.Code, 404, i) 131 //t.Logf("header %v", w.Header()) 132 isGte(t, len(w.Header()), 4, i) 133 isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i) 134 isEqual(t, w.Header().Get("Cache-Control"), "public, max-age=1", i) 135 } 136 } 137 138 func TestServeHTTP200WithGzipAndGzipWithAcceptHeader(t *testing.T) { 139 cases := []struct { 140 n int 141 maxAge time.Duration 142 url, mime, encoding, path, cacheControl string 143 }{ 144 {0, 1, "/css/style1.css", cssMimeType, "xx, gzip, zzz", "assets/css/style1.css.gz", "public, max-age=1"}, 145 {2, 1, "/a/b/css/style1.css", cssMimeType, "xx, gzip, zzz", "assets/css/style1.css.gz", "public, max-age=1"}, 146 {0, 1, "/js/script1.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script1.js.gz", "public, max-age=1"}, 147 {2, 1, "/a/b/js/script1.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script1.js.gz", "public, max-age=1"}, 148 } 149 150 for _, test := range cases { 151 etag := etagFor(test.path) 152 url := mustUrl(test.url) 153 header := newHeader("Accept-Encoding", test.encoding) 154 request := &http.Request{Method: "GET", URL: url, Header: header} 155 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 156 w := httptest.NewRecorder() 157 158 a.ServeHTTP(w, request) 159 160 isEqual(t, w.Code, 200, test.path) 161 headers := w.Header() 162 //t.Logf("%+v\n", headers) 163 isGte(t, len(headers), 7, test.path) 164 isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path) 165 isEqual(t, headers["Content-Type"], []string{test.mime}, test.path) 166 isEqual(t, headers["X-Content-Type-Options"], []string{"nosniff"}, test.path) 167 isEqual(t, headers["Content-Encoding"], []string{"gzip"}, test.path) 168 isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, test.path) 169 isEqual(t, headers["Etag"], []string{"W/" + etag}, test.path) 170 } 171 } 172 173 func TestServeHTTP200WithBrAndBrWithAcceptHeader(t *testing.T) { 174 cases := []struct { 175 n int 176 maxAge time.Duration 177 url, mime, encoding, path, cacheControl string 178 }{ 179 {0, 1, "/css/style1.css", cssMimeType, "br, gzip, zzz", "assets/css/style1.css.br", "public, max-age=1"}, 180 {2, 1, "/a/b/css/style1.css", cssMimeType, "br, gzip, zzz", "assets/css/style1.css.br", "public, max-age=1"}, 181 {0, 1, "/js/script1.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script1.js.br", "public, max-age=1"}, 182 {2, 1, "/a/b/js/script1.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script1.js.br", "public, max-age=1"}, 183 } 184 185 for _, test := range cases { 186 etag := etagFor(test.path) 187 url := mustUrl(test.url) 188 header := newHeader("Accept-Encoding", test.encoding) 189 request := &http.Request{Method: "GET", URL: url, Header: header} 190 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 191 w := httptest.NewRecorder() 192 193 a.ServeHTTP(w, request) 194 195 isEqual(t, w.Code, 200, test.path) 196 headers := w.Header() 197 //t.Logf("%+v\n", headers) 198 isGte(t, len(headers), 7, test.path) 199 isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path) 200 isEqual(t, headers["Content-Type"], []string{test.mime}, test.path) 201 isEqual(t, headers["X-Content-Type-Options"], []string{"nosniff"}, test.path) 202 isEqual(t, headers["Content-Encoding"], []string{"br"}, test.path) 203 isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, test.path) 204 isEqual(t, headers["Etag"], []string{"W/" + etag}, test.path) 205 } 206 } 207 208 func TestServeHTTP200WithGzipButNoAcceptHeader(t *testing.T) { 209 cases := []struct { 210 n int 211 maxAge time.Duration 212 url, mime, encoding, path, cacheControl string 213 }{ 214 {0, 1, "/css/style1.css", cssMimeType, "xx, yy, zzz", "assets/css/style1.css", "public, max-age=1"}, 215 {2, 2, "/a/b/css/style1.css", cssMimeType, "xx, yy, zzz", "assets/css/style1.css", "public, max-age=2"}, 216 {0, 3, "/js/script1.js", javascriptMimeType, "xx, yy, zzz", "assets/js/script1.js", "public, max-age=3"}, 217 {2, 4, "/a/b/js/script1.js", javascriptMimeType, "xx, yy, zzz", "assets/js/script1.js", "public, max-age=4"}, 218 } 219 220 for _, test := range cases { 221 etag := etagFor(test.path) 222 url := mustUrl(test.url) 223 header := newHeader("Accept-Encoding", test.encoding) 224 request := &http.Request{Method: "GET", URL: url, Header: header} 225 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 226 w := httptest.NewRecorder() 227 228 a.ServeHTTP(w, request) 229 230 isEqual(t, w.Code, 200, test.path) 231 headers := w.Header() 232 //t.Logf("%+v\n", headers) 233 isGte(t, len(headers), 6, test.path) 234 isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path) 235 isEqual(t, headers["Content-Type"], []string{test.mime}, test.path) 236 isEqual(t, headers["Content-Encoding"], emptyStrings, test.path) 237 isEqual(t, headers["Vary"], emptyStrings, test.path) 238 isEqual(t, headers["Etag"], []string{etag}, test.path) 239 } 240 } 241 242 func TestServeHTTP200WithGzipAcceptHeaderButNoGzippedFile(t *testing.T) { 243 cases := []struct { 244 n int 245 maxAge time.Duration 246 url, mime, encoding, path, cacheControl string 247 }{ 248 {0, 1, "/css/style2.css", cssMimeType, "xx, gzip, zzz", "assets/css/style2.css", "public, max-age=1"}, 249 {0, 1, "/css/style2.css", cssMimeType, "br, gzip, zzz", "assets/css/style2.css", "public, max-age=1"}, 250 {2, 2, "/a/b/css/style2.css", cssMimeType, "xx, gzip, zzz", "assets/css/style2.css", "public, max-age=2"}, 251 {2, 2, "/a/b/css/style2.css", cssMimeType, "br, gzip, zzz", "assets/css/style2.css", "public, max-age=2"}, 252 {0, 3, "/js/script2.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script2.js", "public, max-age=3"}, 253 {0, 3, "/js/script2.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script2.js", "public, max-age=3"}, 254 {2, 4, "/a/b/js/script2.js", javascriptMimeType, "xx, gzip, zzz", "assets/js/script2.js", "public, max-age=4"}, 255 {2, 4, "/a/b/js/script2.js", javascriptMimeType, "br, gzip, zzz", "assets/js/script2.js", "public, max-age=4"}, 256 {0, 5, "/img/sort_asc.png", "image/png", "xx, gzip, zzz", "assets/img/sort_asc.png", "public, max-age=5"}, 257 {0, 5, "/img/sort_asc.png", "image/png", "br, gzip, zzz", "assets/img/sort_asc.png", "public, max-age=5"}, 258 {2, 6, "/a/b/img/sort_asc.png", "image/png", "xx, gzip, zzz", "assets/img/sort_asc.png", "public, max-age=6"}, 259 {2, 6, "/a/b/img/sort_asc.png", "image/png", "br, gzip, zzz", "assets/img/sort_asc.png", "public, max-age=6"}, 260 } 261 262 for _, test := range cases { 263 etag := etagFor(test.path) 264 url := mustUrl(test.url) 265 header := newHeader("Accept-Encoding", test.encoding) 266 request := &http.Request{Method: "GET", URL: url, Header: header} 267 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).StripOff(test.n).WithMaxAge(test.maxAge * time.Second) 268 w := httptest.NewRecorder() 269 270 a.ServeHTTP(w, request) 271 272 isEqual(t, w.Code, 200, test.path) 273 headers := w.Header() 274 //t.Logf("%+v\n", headers) 275 isGte(t, len(headers), 6, test.path) 276 isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path) 277 isEqual(t, headers["Content-Type"], []string{test.mime}, test.path) 278 isEqual(t, headers["Content-Encoding"], emptyStrings, test.path) 279 isEqual(t, headers["Vary"], emptyStrings, test.path) 280 isEqual(t, headers["Etag"], []string{etag}, test.path) 281 } 282 } 283 284 //------------------------------------------------------------------------------------------------- 285 286 type h404 struct{} 287 288 func (h *h404) ServeHTTP(w http.ResponseWriter, r *http.Request) { 289 w.Header().Set("Content-Type", "text/html") 290 w.WriteHeader(404) 291 w.Write([]byte("<html>foo</html>")) 292 } 293 294 func Test404Handler(t *testing.T) { 295 cases := []struct { 296 path, conType, response string 297 notFound http.Handler 298 }{ 299 {"/img/nonexisting.png", "text/plain; charset=utf-8", "404 Not found\n", nil}, 300 {"/img/nonexisting.png", "text/html", "<html>foo</html>", &h404{}}, 301 } 302 303 for i, test := range cases { 304 url := mustUrl("" + test.path) 305 request := &http.Request{Method: "GET", URL: url} 306 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).WithNotFound(test.notFound) 307 isEqual(t, a.NotFound, test.notFound, i) 308 w := httptest.NewRecorder() 309 310 a.ServeHTTP(w, request) 311 312 isEqual(t, w.Code, 404, i) 313 isEqual(t, w.Header().Get("Content-Type"), test.conType, i) 314 isEqual(t, w.Body.String(), test.response, i) 315 } 316 } 317 318 func Test403Handling(t *testing.T) { 319 cases := []struct { 320 path string 321 header http.Header 322 }{ 323 {"/css/style1.css", newHeader()}, 324 {"/css/style1.css", newHeader("Accept-Encoding", "gzip")}, 325 } 326 327 for i, test := range cases { 328 url := mustUrl("" + test.path) 329 request := &http.Request{Method: "GET", URL: url, Header: test.header} 330 a := NewAssetHandlerFS(&fs403{os.ErrPermission}) 331 w := httptest.NewRecorder() 332 333 a.ServeHTTP(w, request) 334 335 isEqual(t, w.Code, 403, i) 336 isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i) 337 isEqual(t, w.Body.String(), "403 Forbidden\n", i) 338 } 339 } 340 341 func Test503Handling(t *testing.T) { 342 cases := []struct { 343 path string 344 header http.Header 345 }{ 346 {"/css/style1.css", newHeader()}, 347 {"/css/style1.css", newHeader("Accept-Encoding", "gzip")}, 348 } 349 350 for i, test := range cases { 351 url := mustUrl("" + test.path) 352 request := &http.Request{Method: "GET", URL: url, Header: test.header} 353 a := NewAssetHandlerFS(&fs403{os.ErrInvalid}) 354 w := httptest.NewRecorder() 355 356 a.ServeHTTP(w, request) 357 358 isEqual(t, w.Code, 503, i) 359 isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i) 360 isNotEqual(t, w.Header().Get("Retry-After"), "", i) 361 isEqual(t, w.Body.String(), "503 Service unavailable\n", i) 362 } 363 } 364 365 //------------------------------------------------------------------------------------------------- 366 367 func TestServeHTTP304(t *testing.T) { 368 cases := []struct { 369 url, path, encoding string 370 notFound http.Handler 371 }{ 372 {"/css/style1.css", "assets/css/style1.css.gz", "gzip", nil}, 373 {"/css/style1.css", "assets/css/style1.css.br", "br", nil}, 374 {"/css/style2.css", "assets/css/style2.css", "xx", nil}, 375 {"/img/sort_asc.png", "assets/img/sort_asc.png", "xx", nil}, 376 {"/js/script1.js", "assets/js/script1.js.gz", "gzip", nil}, 377 {"/js/script1.js", "assets/js/script1.js.br", "br", nil}, 378 {"/js/script2.js", "assets/js/script2.js", "xx", nil}, 379 380 {"/css/style1.css", "assets/css/style1.css.gz", "gzip", &h404{}}, 381 {"/css/style1.css", "assets/css/style1.css.br", "br", &h404{}}, 382 {"/css/style2.css", "assets/css/style2.css", "xx", &h404{}}, 383 {"/img/sort_asc.png", "assets/img/sort_asc.png", "xx", &h404{}}, 384 {"/js/script1.js", "assets/js/script1.js.gz", "gzip", &h404{}}, 385 {"/js/script1.js", "assets/js/script1.js.br", "br", &h404{}}, 386 {"/js/script2.js", "assets/js/script2.js", "xx", &h404{}}, 387 } 388 389 // net/http serveFiles handles conditional requests according to RFC723x specs. 390 // So we only need to check that a conditional request is correctly wired in. 391 392 for i, test := range cases { 393 etag := etagFor(test.path) 394 url := mustUrl(test.url) 395 header := newHeader("Accept-Encoding", test.encoding, "If-None-Match", etag) 396 request := &http.Request{Method: "GET", URL: url, Header: header} 397 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).WithNotFound(test.notFound) 398 w := httptest.NewRecorder() 399 400 a.ServeHTTP(w, request) 401 402 isEqual(t, w.Code, 304, i) 403 isEqual(t, request.URL.Path, test.url, i) 404 headers := w.Header() 405 //t.Logf("%+v\n", headers) 406 isGte(t, len(headers), 1, i) 407 isEqual(t, headers["Cache-Control"], emptyStrings, i) 408 isEqual(t, headers["Content-Type"], emptyStrings, i) 409 isEqual(t, headers["Content-Length"], emptyStrings, i) 410 if strings.HasSuffix(test.path, ".gz") { 411 isEqual(t, headers["Content-Encoding"], []string{"gzip"}, i) 412 isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, i) 413 isEqual(t, headers["Etag"], []string{"W/" + etag}, i) 414 } else if strings.HasSuffix(test.path, ".br") { 415 isEqual(t, headers["Content-Encoding"], []string{"br"}, i) 416 isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, i) 417 isEqual(t, headers["Etag"], []string{"W/" + etag}, i) 418 } else { 419 isEqual(t, headers["Content-Encoding"], emptyStrings, i) 420 isEqual(t, headers["Vary"], emptyStrings, i) 421 isEqual(t, headers["Etag"], []string{etag}, i) 422 } 423 } 424 } 425 426 func TestSPA(t *testing.T) { 427 cases := []struct { 428 path, conType, response string 429 code int 430 headers map[string][]string 431 }{ 432 {"/img/nonexisting", "text/html; charset=utf-8", "<html></html>", 200, map[string][]string{ 433 "Cache-Control": {"no-store, max-age=0"}, 434 }}, 435 {"/", "text/html; charset=utf-8", "<html></html>", 200, map[string][]string{ 436 "Cache-Control": {"no-store, max-age=0"}, 437 }}, 438 {"/index.html", "", "", 301, map[string][]string{ 439 "Location": {"./"}, 440 }}, 441 {"/img/nonexisting.js", "text/plain; charset=utf-8", "404 Not found\n", 404, map[string][]string{ 442 "Cache-Control": {"public, max-age=1"}, 443 }}, 444 {"/img.de/nonexisting", "text/html; charset=utf-8", "<html></html>", 200, map[string][]string{ 445 "Cache-Control": {"no-store, max-age=0"}, 446 }}, 447 } 448 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).WithSPA().WithMaxAge(1 * time.Second) 449 for i, test := range cases { 450 url := mustUrl("http://localhost:8001" + test.path) 451 request := &http.Request{Method: "GET", URL: url} 452 isEqual(t, a.Spa, true, i) 453 w := httptest.NewRecorder() 454 455 a.ServeHTTP(w, request) 456 log.Printf(w.Body.String()) 457 isEqual(t, w.Code, test.code, i) 458 isEqual(t, w.Header().Get("Content-Type"), test.conType, i) 459 isEqual(t, w.Body.String(), test.response, i) 460 461 if test.headers != nil { 462 headers := w.Header() 463 for header, strings := range test.headers { 464 isEqual(t, headers[header], strings, i) 465 } 466 } 467 } 468 } 469 470 func TestCacheDirectives(t *testing.T) { 471 cases := []struct { 472 handler *Assets 473 path string 474 cacheControl string 475 }{ 476 // No duration set 477 {NewAssetHandler("./assets/").WithCacheDirective(CacheDirectiveImmutable), "/css/style1.css", ""}, 478 {NewAssetHandler("./assets/").WithMaxAge(10 * time.Second), "/css/style1.css", "public, max-age=10"}, 479 {NewAssetHandler("./assets/").WithMaxAge(10 * time.Second).WithCacheDirective(CacheDirectivePrivate), "/css/style1.css", "private, max-age=10"}, 480 {NewAssetHandler("./assets/").WithMaxAge(10 * time.Second).WithCacheDirective(CacheDirectiveImmutable), "/css/style1.css", "immutable, max-age=10"}, 481 } 482 for i, test := range cases { 483 url := mustUrl("http://localhost:8081" + test.path) 484 request := &http.Request{Method: "GET", URL: url} 485 w := httptest.NewRecorder() 486 487 test.handler.ServeHTTP(w, request) 488 isEqual(t, w.Header().Get("Cache-Control"), test.cacheControl, i) 489 } 490 } 491 492 //------------------------------------------------------------------------------------------------- 493 494 func Benchmark(t *testing.B) { 495 t.StopTimer() 496 497 cases := []struct { 498 strip int 499 url, enc string 500 sendEtagFor string 501 code int 502 }{ 503 {0, "css/style1.css", "gzip", "", 200}, // has Gzip 504 {0, "css/style1.css", "br", "", 200}, // has Brotli 505 {1, "a/css/style1.css", "gzip", "", 200}, // has Gzip 506 {1, "a/css/style1.css", "br", "", 200}, // has Brotli 507 {2, "a/b/css/style1.css", "gzip", "", 200}, // has Gzip 508 {2, "a/b/css/style1.css", "br", "", 200}, // has Brotli 509 {2, "a/b/css/style1.css", "xxxx", "", 200}, // has Gzip 510 {2, "a/b/css/style1.css", "gzip", "assets/css/style1.css.gz", 304}, // has Gzip 511 {2, "a/b/css/style1.css", "br", "assets/css/style1.css.br", 304}, // has Brotli 512 {2, "a/b/css/style1.css", "xxxx", "assets/css/style1.css", 304}, // has Gzip 513 514 {2, "a/b/css/style2.css", "gzip", "", 200}, 515 {2, "a/b/css/style2.css", "xxxx", "", 200}, 516 {2, "a/b/css/style2.css", "gzip", "assets/css/style2.css", 304}, 517 {2, "a/a/css/style2.css", "xxxx", "assets/css/style2.css", 304}, 518 519 {2, "a/b/js/script1.js", "gzip", "", 200}, // has gzip 520 {2, "a/b/js/script1.js", "br", "", 200}, // has Brotli 521 {2, "a/b/js/script1.js", "xxxx", "", 200}, // has gzip 522 {2, "a/b/js/script1.js", "gzip", "assets/js/script1.js.gz", 304}, // has gzip 523 {2, "a/b/js/script1.js", "br", "assets/js/script1.js.br", 304}, // has Brotli 524 {2, "a/a/js/script1.js", "xxxx", "assets/js/script1.js", 304}, // has gzip 525 526 {2, "a/b/js/script2.js", "gzip", "", 200}, 527 {2, "a/b/js/script2.js", "xxxx", "", 200}, 528 {2, "a/b/js/script2.js", "gzip", "assets/js/script2.js", 304}, 529 {2, "a/a/js/script2.js", "xxxx", "assets/js/script2.js", 304}, 530 531 {2, "a/b/img/sort_asc.png", "gzip", "", 200}, 532 {2, "a/b/img/sort_asc.png", "xxxx", "", 200}, 533 {2, "a/b/img/sort_asc.png", "gzip", "assets/img/sort_asc.png", 304}, 534 {2, "a/a/img/sort_asc.png", "xxxx", "assets/img/sort_asc.png", 304}, 535 536 {2, "a/b/img/nonexisting.png", "gzip", "", 404}, 537 {2, "a/b/img/nonexisting.png", "xxxx", "", 404}, 538 } 539 540 ages := []time.Duration{0, time.Hour} 541 542 for _, test := range cases { 543 header := newHeader("Accept-Encoding", test.enc) 544 etagOn := "no-etag" 545 if test.sendEtagFor != "" { 546 header = newHeader("Accept-Encoding", test.enc, "If-None-Match", etagFor(test.sendEtagFor)) 547 etagOn = "etag" 548 } 549 550 for _, age := range ages { 551 a := NewAssetHandler("./assets/").WithCacheDirective(CacheDirectivePublic).StripOff(test.strip).WithMaxAge(age) 552 553 t.Run(fmt.Sprintf("%s~%s~%v~%d~%v", test.url, test.enc, etagOn, test.code, age), func(b *testing.B) { 554 b.StopTimer() 555 556 for i := 0; i < b.N; i++ { 557 url := mustUrl("/" + test.url) 558 request := &http.Request{Method: "GET", URL: url, Header: header} 559 w := httptest.NewRecorder() 560 561 b.StartTimer() 562 a.ServeHTTP(w, request) 563 b.StopTimer() 564 565 if w.Code != test.code { 566 b.Fatalf("Expected %d but got %d", test.code, w.Code) 567 } 568 } 569 }) 570 } 571 } 572 } 573 574 //------------------------------------------------------------------------------------------------- 575 576 func isEqual(t *testing.T, a, b, hint interface{}) { 577 t.Helper() 578 if !reflect.DeepEqual(a, b) { 579 t.Errorf("Got %#v; expected %#v - for %v\n", a, b, hint) 580 } 581 } 582 583 func isNotEqual(t *testing.T, a, b, hint interface{}) { 584 t.Helper() 585 if reflect.DeepEqual(a, b) { 586 t.Errorf("Got %#v; expected something else - for %v\n", a, hint) 587 } 588 } 589 590 func isGte(t *testing.T, a, b int, hint interface{}) { 591 t.Helper() 592 if a < b { 593 t.Errorf("Got %d; expected at least %d - for %v\n", a, b, hint) 594 } 595 } 596 597 func mustUrl(s string) *URL { 598 parsed, err := Parse(s) 599 must(err) 600 return parsed 601 } 602 603 func newHeader(kv ...string) http.Header { 604 header := make(http.Header) 605 for i, x := range kv { 606 if i%2 == 0 { 607 header[x] = []string{kv[i+1]} 608 } 609 } 610 return header 611 } 612 613 // must abort the program on error, printing a stack trace. 614 func must(err error) { 615 if err != nil { 616 panic(err) 617 } 618 } 619 620 func mustStat(name string) os.FileInfo { 621 d, err := os.Stat(name) 622 if err != nil { 623 panic(err) 624 } 625 return d 626 } 627 628 func etagFor(name string) string { 629 d := mustStat(name) 630 t := "" 631 return fmt.Sprintf(`%s"%x-%x"`, t, d.ModTime().Unix(), d.Size()) 632 } 633 634 //------------------------------------------------------------------------------------------------- 635 636 type fs403 struct { 637 err error 638 } 639 640 func (fs fs403) Create(name string) (afero.File, error) { 641 return nil, fs.err 642 } 643 644 func (fs fs403) Mkdir(name string, perm os.FileMode) error { 645 return fs.err 646 } 647 648 func (fs fs403) MkdirAll(path string, perm os.FileMode) error { 649 return fs.err 650 } 651 652 func (fs fs403) Open(name string) (afero.File, error) { 653 return nil, fs.err 654 } 655 656 func (fs fs403) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) { 657 return nil, fs.err 658 } 659 660 func (fs fs403) Remove(name string) error { 661 return fs.err 662 } 663 664 func (fs fs403) RemoveAll(path string) error { 665 return fs.err 666 } 667 668 func (fs fs403) Rename(oldname, newname string) error { 669 return fs.err 670 } 671 672 func (fs fs403) Stat(name string) (os.FileInfo, error) { 673 return nil, fs.err 674 } 675 676 func (fs403) Name() string { 677 return "dumb" 678 } 679 680 func (fs fs403) Chmod(name string, mode os.FileMode) error { 681 return fs.err 682 } 683 684 func (fs fs403) Chown(name string, uid, gid int) error { 685 return fs.err 686 } 687 688 func (fs fs403) Chtimes(name string, atime time.Time, mtime time.Time) error { 689 return fs.err 690 }