github.com/jyd519/zipfs@v1.2.1/file_server_test.go (about) 1 package zipfs 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "mime" 8 "net/http" 9 "net/url" 10 "os" 11 "path" 12 "runtime" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 ) 20 21 func init() { 22 _, filename, _, _ := runtime.Caller(0) 23 // The ".." may change depending on you folder structure 24 dir := path.Join(path.Dir(filename)) 25 fmt.Println(dir) 26 err := os.Chdir(dir) 27 if err != nil { 28 panic(err) 29 } 30 } 31 32 type TestResponseWriter struct { 33 header http.Header 34 status int 35 buf bytes.Buffer 36 } 37 38 func NewTestResponseWriter() *TestResponseWriter { 39 return &TestResponseWriter{ 40 header: make(http.Header), 41 status: 200, 42 } 43 } 44 45 func (w *TestResponseWriter) Header() http.Header { 46 return w.header 47 } 48 49 func (w *TestResponseWriter) Write(b []byte) (int, error) { 50 return w.buf.Write(b) 51 } 52 53 func (w *TestResponseWriter) WriteHeader(status int) { 54 w.status = status 55 } 56 57 func TestNew(t *testing.T) { 58 assert := assert.New(t) 59 testCases := []struct { 60 Name string 61 Error string 62 }{ 63 { 64 Name: "testdata/does-not-exist.zip", 65 Error: "The system cannot find the file specified", 66 }, 67 { 68 Name: "testdata/testdata.zip", 69 Error: "", 70 }, 71 { 72 Name: "testdata/not-a-zip-file.txt", 73 Error: "zip: not a valid zip file", 74 }, 75 } 76 77 for _, tc := range testCases { 78 fs, err := New(tc.Name) 79 if tc.Error != "" { 80 assert.Error(err) 81 //assert.True(strings.Contains(err.Error(), tc.Error), err.Error()) 82 assert.Nil(fs) 83 } else { 84 assert.NoError(err) 85 assert.NotNil(fs) 86 } 87 if fs != nil { 88 fs.Close() 89 } 90 } 91 } 92 93 func TestServeHTTP(t *testing.T) { 94 assert := assert.New(t) 95 require := require.New(t) 96 97 fs, err := New("testdata/testdata.zip") 98 require.NoError(err) 99 require.NotNil(fs) 100 101 handler := FileServer(fs) 102 103 testCases := []struct { 104 Path string 105 Headers []string 106 Status int 107 ContentType string 108 ContentLength string 109 ContentEncoding string 110 ETag string 111 Size int 112 Location string 113 }{ 114 { 115 Path: "/img/circle.png", 116 Status: 200, 117 Headers: []string{ 118 "Accept-Encoding: deflate, gzip", 119 }, 120 ContentType: "image/png", 121 ContentLength: "5973", 122 ContentEncoding: "", 123 Size: 5973, 124 ETag: `"1755529fb2ff"`, 125 }, 126 { 127 Path: "/img/circle.png", 128 Status: 200, 129 Headers: []string{ 130 "Accept-Encoding: gzip", 131 }, 132 ContentType: "image/png", 133 ContentLength: "5973", 134 ContentEncoding: "", 135 Size: 5973, 136 ETag: `"1755529fb2ff"`, 137 }, 138 { 139 Path: "/", 140 Status: 200, 141 Headers: []string{ 142 "Accept-Encoding: deflate, gzip", 143 }, 144 ContentType: "text/html; charset=utf-8", 145 ContentEncoding: "deflate", 146 }, 147 { 148 Path: "/test.html", 149 Status: 200, 150 Headers: []string{}, 151 ContentType: "text/html; charset=utf-8", 152 ContentEncoding: "", 153 }, 154 { 155 Path: "/does/not/exist", 156 Status: 404, 157 Headers: []string{ 158 "Accept-Encoding: deflate, gzip", 159 }, 160 ContentType: "text/plain; charset=utf-8", 161 }, 162 { 163 Path: "/random.dat", 164 Status: 200, 165 Headers: []string{ 166 "Accept-Encoding: deflate", 167 }, 168 ContentType: getMimeType(".dat"), 169 ContentLength: "10000", 170 ContentEncoding: "", 171 Size: 10000, 172 ETag: `"27106c15f45b"`, 173 }, 174 { 175 Path: "/random.dat", 176 Status: 200, 177 Headers: []string{}, 178 ContentType: getMimeType(".dat"), 179 ContentLength: "10000", 180 ContentEncoding: "", 181 Size: 10000, 182 ETag: `"27106c15f45b"`, 183 }, 184 { 185 Path: "/random.dat", 186 Status: 206, 187 Headers: []string{ 188 `If-Range: "27106c15f45b"`, 189 "Range: bytes=0-499", 190 }, 191 ContentType: getMimeType(".dat"), 192 ContentLength: "500", 193 ContentEncoding: "", 194 Size: 500, 195 ETag: `"27106c15f45b"`, 196 }, 197 { 198 Path: "/random.dat", 199 Status: 206, 200 Headers: []string{ 201 `If-Range: "27106c15f45b"`, 202 "Range: bytes=0-499", 203 }, 204 ContentType: getMimeType(".dat"), 205 ContentLength: "500", 206 ContentEncoding: "", 207 Size: 500, 208 ETag: `"27106c15f45b"`, 209 }, 210 { 211 Path: "/random.dat", 212 Status: 200, 213 Headers: []string{ 214 `If-Range: "123456789"`, 215 "Range: bytes=0-499", 216 "Accept-Encoding: deflate, gzip", 217 }, 218 ContentType: getMimeType(".dat"), 219 ContentLength: "10000", 220 ContentEncoding: "", 221 Size: 10000, 222 ETag: `"27106c15f45b"`, 223 }, 224 { 225 Path: "/random.dat", 226 Status: 304, 227 Headers: []string{ 228 `If-None-Match: "27106c15f45b"`, 229 "Accept-Encoding: deflate, gzip", 230 }, 231 ContentType: "", 232 ContentLength: "", 233 ContentEncoding: "", 234 Size: 0, 235 ETag: `"27106c15f45b"`, 236 }, 237 { 238 Path: "/random.dat", 239 Status: 304, 240 Headers: []string{ 241 fmt.Sprintf("If-Modified-Since: %s", time.Now().UTC().Add(time.Hour*10000).Format(http.TimeFormat)), 242 "Accept-Encoding: deflate, gzip", 243 }, 244 ContentType: "", 245 ContentLength: "", 246 ContentEncoding: "", 247 Size: 0, 248 }, 249 { 250 Path: "random.dat", 251 Status: 200, 252 Headers: []string{}, 253 ContentType: getMimeType(".dat"), 254 ContentLength: "10000", 255 Size: 10000, 256 ETag: `"27106c15f45b"`, 257 }, 258 { 259 Path: "/index.html", 260 Status: 301, 261 Headers: []string{}, 262 Location: "./", 263 }, 264 { 265 Path: "/empty", 266 Status: 301, 267 Headers: []string{}, 268 Location: "empty/", 269 }, 270 { 271 Path: "/img/circle.png/", 272 Status: 301, 273 Headers: []string{}, 274 Location: "../circle.png", 275 }, 276 { 277 Path: "/empty/", 278 Status: 403, 279 ContentType: "text/plain; charset=utf-8", 280 Headers: []string{}, 281 }, 282 } 283 284 for _, tc := range testCases { 285 req := &http.Request{ 286 URL: &url.URL{ 287 Scheme: "http", 288 Host: "test-server.com", 289 Path: tc.Path, 290 }, 291 Header: make(http.Header), 292 Method: "GET", 293 } 294 295 for _, header := range tc.Headers { 296 arr := strings.SplitN(header, ":", 2) 297 key := strings.TrimSpace(arr[0]) 298 value := strings.TrimSpace(arr[1]) 299 req.Header.Add(key, value) 300 } 301 302 w := NewTestResponseWriter() 303 handler.ServeHTTP(w, req) 304 305 assert.Equal(tc.Status, w.status, tc.Path) 306 assert.Equal(tc.ContentType, w.Header().Get("Content-Type"), tc.Path) 307 if tc.ContentLength != "" { 308 // only check content length for non-text because length will differ 309 // between windows and unix 310 assert.Equal(tc.ContentLength, w.Header().Get("Content-Length"), tc.Path) 311 } 312 // TODO: serve zip files in a zip causes zlib header error 313 // assert.Equal(tc.ContentEncoding, w.Header().Get("Content-Encoding"), tc.Path) 314 // if tc.Size > 0 { 315 // assert.Equal(tc.Size, w.buf.Len(), tc.Path) 316 // } 317 if tc.ETag != "" { 318 // only check ETag for non-text files because CRC will differ between 319 // windows and unix 320 assert.Equal(tc.ETag, w.Header().Get("Etag"), tc.Path) 321 } 322 if tc.Location != "" { 323 assert.Equal(tc.Location, w.Header().Get("Location"), tc.Path) 324 } 325 } 326 } 327 328 func TestToHTTPError(t *testing.T) { 329 assert := assert.New(t) 330 331 testCases := []struct { 332 Err error 333 Message string 334 Status int 335 }{ 336 { 337 Err: os.ErrNotExist, 338 Message: "404 page not found", 339 Status: 404, 340 }, 341 { 342 Err: os.ErrPermission, 343 Message: "403 Forbidden", 344 Status: 403, 345 }, 346 { 347 Err: errors.New("test error condition"), 348 Message: "500 Internal Server Error", 349 Status: 500, 350 }, 351 } 352 353 for _, tc := range testCases { 354 msg, code := toHTTPError(tc.Err) 355 assert.Equal(tc.Message, msg, tc.Err.Error()) 356 assert.Equal(tc.Status, code, tc.Err.Error()) 357 msg, code = toHTTPError(&os.PathError{Op: "op", Path: "path", Err: tc.Err}) 358 assert.Equal(tc.Message, msg, tc.Err.Error()) 359 assert.Equal(tc.Status, code, tc.Err.Error()) 360 } 361 } 362 363 func TestLocalRedirect(t *testing.T) { 364 assert := assert.New(t) 365 366 testCases := []struct { 367 Url string 368 NewPath string 369 Location string 370 }{ 371 { 372 Url: "/test", 373 NewPath: "./test/", 374 Location: "./test/", 375 }, 376 { 377 Url: "/test?a=32&b=54", 378 NewPath: "./test/", 379 Location: "./test/?a=32&b=54", 380 }, 381 } 382 383 for _, tc := range testCases { 384 u, err := url.Parse(tc.Url) 385 assert.NoError(err) 386 r := &http.Request{ 387 URL: u, 388 } 389 w := NewTestResponseWriter() 390 localRedirect(w, r, tc.NewPath) 391 assert.Equal(http.StatusMovedPermanently, w.status) 392 assert.Equal(tc.Location, w.Header().Get("Location")) 393 } 394 } 395 396 func TestCheckETag(t *testing.T) { 397 assert := assert.New(t) 398 399 testCases := []struct { 400 ModTime time.Time 401 Method string 402 Etag string 403 Range string 404 IfRange string 405 IfNoneMatch string 406 ContentType string 407 ContentLength string 408 409 RangeReq string 410 Done bool 411 }{ 412 { 413 // Using the modified time instead of the ETag in If-Range header 414 // If-None-Match is not set. 415 ModTime: time.Date(2006, 4, 12, 15, 4, 5, 0, time.UTC), 416 Method: "GET", 417 Etag: `"xxxxyyyy"`, 418 Range: "bytes=500-999", 419 IfRange: `Wed, 12 Apr 2006 15:04:05 GMT`, 420 ContentType: "text/html", 421 ContentLength: "2024", 422 423 RangeReq: "bytes=500-999", 424 Done: false, 425 }, 426 { 427 // Using the modified time instead of the ETag in If-Range header 428 // If-None-Match is set. 429 ModTime: time.Date(2006, 4, 12, 15, 4, 5, 0, time.UTC), 430 Method: "GET", 431 Etag: `"xxxxyyyy"`, 432 Range: "bytes=500-999", 433 IfRange: `Wed, 12 Apr 2006 15:04:05 GMT`, 434 IfNoneMatch: `"xxxxyyyy"`, 435 ContentType: "text/html", 436 ContentLength: "2024", 437 438 RangeReq: "", 439 Done: true, 440 }, 441 { 442 // ETag not set, but If-None-Match is. 443 ModTime: time.Date(2006, 4, 12, 15, 4, 5, 0, time.UTC), 444 Method: "GET", 445 IfNoneMatch: `"xxxxyyyy"`, 446 ContentType: "text/html", 447 ContentLength: "2024", 448 449 RangeReq: "", 450 Done: false, 451 }, 452 { 453 // ETag matches If-None-Match, but method is not GET or HEAD 454 ModTime: time.Date(2006, 4, 12, 15, 4, 5, 0, time.UTC), 455 Method: "POST", 456 Etag: `"xxxxyyyy"`, 457 IfNoneMatch: `"xxxxyyyy"`, 458 ContentType: "text/html", 459 ContentLength: "2024", 460 461 RangeReq: "", 462 Done: false, 463 }, 464 { 465 // Using the ETag in the If-Range header 466 ModTime: time.Date(2006, 4, 12, 15, 4, 5, 0, time.UTC), 467 Method: "GET", 468 Etag: `"xxxxyyyy"`, 469 Range: "bytes=500-999", 470 IfRange: `"xxxxyyyy"`, 471 ContentType: "text/html", 472 ContentLength: "2024", 473 474 RangeReq: "bytes=500-999", 475 Done: false, 476 }, 477 { 478 // Using an out of date ETag in the If-Range header 479 ModTime: time.Date(2006, 4, 12, 15, 4, 5, 0, time.UTC), 480 Method: "GET", 481 Etag: `"xxxxyyyy"`, 482 Range: "bytes=500-999", 483 IfRange: `"aaaabbbb"`, 484 ContentType: "text/html", 485 ContentLength: "2024", 486 487 RangeReq: "", 488 Done: false, 489 }, 490 { 491 // Using an out of date ETag in the If-Range header 492 ModTime: time.Date(2006, 4, 12, 15, 4, 5, 0, time.UTC), 493 Method: "GET", 494 Etag: `"xxxxyyyy"`, 495 Range: "bytes=500-999", 496 IfRange: `"aaaabbbb"`, 497 ContentType: "text/html", 498 ContentLength: "2024", 499 500 RangeReq: "", 501 Done: false, 502 }, 503 } 504 505 for i, tc := range testCases { 506 r := &http.Request{Method: tc.Method, Header: http.Header{}} 507 w := NewTestResponseWriter() 508 if tc.Etag != "" { 509 w.Header().Add("Etag", tc.Etag) 510 } 511 if tc.Range != "" { 512 r.Header.Add("Range", tc.Range) 513 } 514 if tc.IfRange != "" { 515 r.Header.Add("If-Range", tc.IfRange) 516 } 517 if tc.IfNoneMatch != "" { 518 r.Header.Add("If-None-Match", tc.IfNoneMatch) 519 } 520 if tc.ContentType != "" { 521 w.Header().Add("Content-Type", tc.ContentType) 522 } 523 if tc.ContentLength != "" { 524 w.Header().Add("Content-Length", tc.ContentLength) 525 } 526 _ = "breakpoint" 527 rangeReq, done := checkETag(w, r, tc.ModTime) 528 assert.Equal(tc.RangeReq, rangeReq, fmt.Sprintf("test case #%d", i)) 529 assert.Equal(tc.Done, done, fmt.Sprintf("test case #%d", i)) 530 if done { 531 assert.Equal("", w.Header().Get("Content-Length")) 532 assert.Equal("", w.Header().Get("Content-Type")) 533 } else { 534 assert.Equal(tc.ContentLength, w.Header().Get("Content-Length")) 535 assert.Equal(tc.ContentType, w.Header().Get("Content-Type")) 536 } 537 } 538 } 539 540 func TestCheckLastModified(t *testing.T) { 541 assert := assert.New(t) 542 543 testCases := []struct { 544 ModTime time.Time 545 IfModifiedSince string 546 ContentType string 547 ContentLength string 548 LastModified string 549 Status int 550 Done bool 551 }{ 552 { 553 ModTime: time.Date(2020, 8, 1, 15, 3, 41, 0, time.UTC), 554 IfModifiedSince: "Sat, 01 Aug 2020 15:03:41 GMT", 555 ContentType: "text/html", 556 ContentLength: "3000", 557 Status: http.StatusNotModified, 558 Done: true, 559 }, 560 { 561 ModTime: time.Date(2020, 8, 1, 15, 3, 41, 0, time.UTC), 562 IfModifiedSince: "Sat, 01 Aug 2020 15:03:40 GMT", 563 ContentType: "text/html", 564 ContentLength: "3000", 565 LastModified: "Sat, 01 Aug 2020 15:03:41 GMT", 566 Status: http.StatusOK, 567 Done: false, 568 }, 569 { 570 ModTime: time.Time{}, 571 IfModifiedSince: "Sat, 01 Aug 2020 15:03:40 GMT", 572 ContentType: "text/html", 573 ContentLength: "3000", 574 Status: http.StatusOK, 575 Done: false, 576 }, 577 { 578 ModTime: time.Unix(0, 0), 579 IfModifiedSince: "Sat, 01 Aug 2020 15:03:40 GMT", 580 ContentType: "text/html", 581 ContentLength: "3000", 582 Status: http.StatusOK, 583 Done: false, 584 }, 585 } 586 587 for i, tc := range testCases { 588 r := &http.Request{Header: http.Header{}} 589 w := NewTestResponseWriter() 590 if tc.IfModifiedSince != "" { 591 r.Header.Set("If-Modified-Since", tc.IfModifiedSince) 592 } 593 if tc.ContentType != "" { 594 w.Header().Set("Content-Type", tc.ContentType) 595 } 596 if tc.ContentLength != "" { 597 w.Header().Set("Content-Length", tc.ContentLength) 598 } 599 done := checkLastModified(w, r, tc.ModTime) 600 failText := fmt.Sprintf("test case #%d", i) 601 assert.Equal(tc.Done, done, failText) 602 assert.Equal(tc.Status, w.status, failText) 603 if tc.LastModified != "" { 604 assert.Equal(tc.LastModified, w.Header().Get("Last-Modified"), failText) 605 } 606 if done { 607 assert.Equal("", w.Header().Get("Content-Type")) 608 assert.Equal("", w.Header().Get("Content-Length")) 609 } else { 610 assert.Equal(tc.ContentType, w.Header().Get("Content-Type")) 611 assert.Equal(tc.ContentLength, w.Header().Get("Content-Length")) 612 } 613 } 614 } 615 616 func getMimeType(ext string) string { 617 mimeType := mime.TypeByExtension(ext) 618 if mimeType == "" { 619 mimeType = "application/octet-stream" 620 } 621 return mimeType 622 }