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  }