Skip to content

Commit ddd69d1

Browse files
committed
Add tests for fileserver.go
1 parent 8ecc366 commit ddd69d1

File tree

1 file changed

+325
-0
lines changed

1 file changed

+325
-0
lines changed

middleware/fileserver_test.go

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
package middleware
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/http/httptest"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
"testing"
11+
)
12+
13+
var testDir = filepath.Join(os.TempDir(), "caddy_testdir")
14+
var customErr = errors.New("Custom Error")
15+
16+
// testFiles is a map with relative paths to test files as keys and file content as values.
17+
// The map represents the following structure:
18+
// - $TEMP/caddy_testdir/
19+
// '-- file1.html
20+
// '-- dirwithindex/
21+
// '---- index.html
22+
// '-- dir/
23+
// '---- file2.html
24+
// '---- hidden.html
25+
var testFiles = map[string]string{
26+
"file1.html": "<h1>file1.html</h1>",
27+
filepath.Join("dirwithindex", "index.html"): "<h1>dirwithindex/index.html</h1>",
28+
filepath.Join("dir", "file2.html"): "<h1>dir/file2.html</h1>",
29+
filepath.Join("dir", "hidden.html"): "<h1>dir/hidden.html</h1>",
30+
}
31+
32+
// TestServeHTTP covers positive scenarios when serving files.
33+
func TestServeHTTP(t *testing.T) {
34+
35+
beforeServeHttpTest(t)
36+
defer afterServeHttpTest(t)
37+
38+
fileserver := FileServer(http.Dir(testDir), []string{"hidden.html"})
39+
40+
movedPermanently := "Moved Permanently"
41+
42+
tests := []struct {
43+
url string
44+
45+
expectedStatus int
46+
expectedBodyContent string
47+
}{
48+
// Test 0 - access withoutt any path
49+
{
50+
url: "https://foo",
51+
expectedStatus: http.StatusNotFound,
52+
},
53+
// Test 1 - access root (without index.html)
54+
{
55+
url: "https://foo/",
56+
expectedStatus: http.StatusNotFound,
57+
},
58+
// Test 2 - access existing file
59+
{
60+
url: "https://foo/file1.html",
61+
expectedStatus: http.StatusOK,
62+
expectedBodyContent: testFiles["file1.html"],
63+
},
64+
// Test 3 - access folder with index file with trailing slash
65+
{
66+
url: "https://foo/dirwithindex/",
67+
expectedStatus: http.StatusOK,
68+
expectedBodyContent: testFiles[filepath.Join("dirwithindex", "index.html")],
69+
},
70+
// Test 4 - access folder with index file without trailing slash
71+
{
72+
url: "https://foo/dirwithindex",
73+
expectedStatus: http.StatusMovedPermanently,
74+
expectedBodyContent: movedPermanently,
75+
},
76+
// Test 5 - access folder without index file
77+
{
78+
url: "https://foo/dir/",
79+
expectedStatus: http.StatusNotFound,
80+
},
81+
// Test 6 - access folder withtout trailing slash
82+
{
83+
url: "https://foo/dir",
84+
expectedStatus: http.StatusMovedPermanently,
85+
expectedBodyContent: movedPermanently,
86+
},
87+
// Test 6 - access file with trailing slash
88+
{
89+
url: "https://foo/file1.html/",
90+
expectedStatus: http.StatusMovedPermanently,
91+
expectedBodyContent: movedPermanently,
92+
},
93+
// Test 7 - access not existing path
94+
{
95+
url: "https://foo/not_existing",
96+
expectedStatus: http.StatusNotFound,
97+
},
98+
// Test 8 - access a file, marked as hidden
99+
{
100+
url: "https://foo/dir/hidden.html",
101+
expectedStatus: http.StatusNotFound,
102+
},
103+
// Test 9 - access a index file directly
104+
{
105+
url: "https://foo/dirwithindex/index.html",
106+
expectedStatus: http.StatusOK,
107+
expectedBodyContent: testFiles[filepath.Join("dirwithindex", "index.html")],
108+
},
109+
// Test 10 - send a request with query params
110+
{
111+
url: "https://foo/dir?param1=val",
112+
expectedStatus: http.StatusMovedPermanently,
113+
expectedBodyContent: movedPermanently,
114+
},
115+
}
116+
117+
for i, test := range tests {
118+
responseRecorder := httptest.NewRecorder()
119+
request, err := http.NewRequest("GET", test.url, strings.NewReader(""))
120+
status, err := fileserver.ServeHTTP(responseRecorder, request)
121+
122+
// check if error matches expectations
123+
if err != nil {
124+
t.Errorf(getTestPrefix(i)+"Serving file at %s failed. Error was: %v", test.url, err)
125+
}
126+
127+
// check status code
128+
if test.expectedStatus != status {
129+
t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status)
130+
}
131+
132+
// check body content
133+
if !strings.Contains(responseRecorder.Body.String(), test.expectedBodyContent) {
134+
t.Errorf(getTestPrefix(i)+"Expected body to contain %q, found %q", test.expectedBodyContent, responseRecorder.Body.String())
135+
}
136+
}
137+
138+
}
139+
140+
// beforeServeHttpTest creates a test directory with the structure, defined in the variable testFiles
141+
func beforeServeHttpTest(t *testing.T) {
142+
// make the root test dir
143+
err := os.Mkdir(testDir, os.ModePerm)
144+
if err != nil {
145+
if !os.IsExist(err) {
146+
t.Fatalf("Failed to create test dir. Error was: %v", err)
147+
return
148+
}
149+
}
150+
151+
for relFile, fileContent := range testFiles {
152+
absFile := filepath.Join(testDir, relFile)
153+
154+
// make sure the parent directories exist
155+
parentDir := filepath.Dir(absFile)
156+
_, err = os.Stat(parentDir)
157+
if err != nil {
158+
os.MkdirAll(parentDir, os.ModePerm)
159+
}
160+
161+
// now create the test files
162+
f, err := os.Create(absFile)
163+
if err != nil {
164+
t.Fatalf("Failed to create test file %s. Error was: %v", absFile, err)
165+
return
166+
}
167+
168+
// and fill them with content
169+
_, err = f.WriteString(fileContent)
170+
if err != nil {
171+
t.Fatal("Failed to write to %s. Error was: %v", absFile, err)
172+
return
173+
}
174+
f.Close()
175+
}
176+
177+
}
178+
179+
// afterServeHttpTest removes the test dir and all its content
180+
func afterServeHttpTest(t *testing.T) {
181+
// cleans up everything under the test dir. No need to clean the individual files.
182+
err := os.RemoveAll(testDir)
183+
if err != nil {
184+
t.Fatalf("Failed to clean up test dir %s. Error was: %v", testDir, err)
185+
}
186+
}
187+
188+
// failingFS implements the http.FileSystem interface. The Open method always returns the error, assigned to err
189+
type failingFS struct {
190+
err error // the error to return when Open is called
191+
fileImpl http.File // inject the file implementation
192+
}
193+
194+
// Open returns the assigned failingFile and error
195+
func (f failingFS) Open(path string) (http.File, error) {
196+
return f.fileImpl, f.err
197+
}
198+
199+
// failingFile implements http.File but returns a predefined error on every Stat() method call.
200+
type failingFile struct {
201+
http.File
202+
err error
203+
}
204+
205+
// Stat returns nil FileInfo and the provided error on every call
206+
func (ff failingFile) Stat() (os.FileInfo, error) {
207+
return nil, ff.err
208+
}
209+
210+
// Close is noop and returns no error
211+
func (ff failingFile) Close() error {
212+
return nil
213+
}
214+
215+
// TestServeHTTPFailingFS tests error cases where the Open function fails with various errors.
216+
func TestServeHTTPFailingFS(t *testing.T) {
217+
218+
tests := []struct {
219+
fsErr error
220+
expectedStatus int
221+
expectedErr error
222+
expectedHeaders map[string]string
223+
}{
224+
{
225+
fsErr: os.ErrNotExist,
226+
expectedStatus: http.StatusNotFound,
227+
expectedErr: nil,
228+
},
229+
{
230+
fsErr: os.ErrPermission,
231+
expectedStatus: http.StatusForbidden,
232+
expectedErr: os.ErrPermission,
233+
},
234+
{
235+
fsErr: customErr,
236+
expectedStatus: http.StatusServiceUnavailable,
237+
expectedErr: customErr,
238+
expectedHeaders: map[string]string{"Retry-After": "5"},
239+
},
240+
}
241+
242+
for i, test := range tests {
243+
// initialize a file server with the failing FileSystem
244+
fileserver := FileServer(failingFS{err: test.fsErr}, nil)
245+
246+
// prepare the request and response
247+
request, err := http.NewRequest("GET", "https://foo/", nil)
248+
if err != nil {
249+
t.Fatalf("Failed to build request. Error was: %v", err)
250+
}
251+
responseRecorder := httptest.NewRecorder()
252+
253+
status, actualErr := fileserver.ServeHTTP(responseRecorder, request)
254+
255+
// check the status
256+
if status != test.expectedStatus {
257+
t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status)
258+
}
259+
260+
// check the error
261+
if actualErr != test.expectedErr {
262+
t.Errorf(getTestPrefix(i)+"Expected err %v, found %v", test.expectedErr, actualErr)
263+
}
264+
265+
// check the headers - a special case for server under load
266+
if test.expectedHeaders != nil && len(test.expectedHeaders) > 0 {
267+
for expectedKey, expectedVal := range test.expectedHeaders {
268+
actualVal := responseRecorder.Header().Get(expectedKey)
269+
if expectedVal != actualVal {
270+
t.Errorf(getTestPrefix(i)+"Expected header %s: %s, found %s", expectedKey, expectedVal, actualVal)
271+
}
272+
}
273+
}
274+
}
275+
}
276+
277+
// TestServeHTTPFailingStat tests error cases where the initial Open function succeeds, but the Stat method on the opened file fails.
278+
func TestServeHTTPFailingStat(t *testing.T) {
279+
280+
tests := []struct {
281+
statErr error
282+
expectedStatus int
283+
expectedErr error
284+
}{
285+
{
286+
statErr: os.ErrNotExist,
287+
expectedStatus: http.StatusNotFound,
288+
expectedErr: nil,
289+
},
290+
{
291+
statErr: os.ErrPermission,
292+
expectedStatus: http.StatusForbidden,
293+
expectedErr: os.ErrPermission,
294+
},
295+
{
296+
statErr: customErr,
297+
expectedStatus: http.StatusInternalServerError,
298+
expectedErr: customErr,
299+
},
300+
}
301+
302+
for i, test := range tests {
303+
// initialize a file server. The FileSystem will not fail, but calls to the Stat method of the returned File object will
304+
fileserver := FileServer(failingFS{err: nil, fileImpl: failingFile{err: test.statErr}}, nil)
305+
306+
// prepare the request and response
307+
request, err := http.NewRequest("GET", "https://foo/", nil)
308+
if err != nil {
309+
t.Fatalf("Failed to build request. Error was: %v", err)
310+
}
311+
responseRecorder := httptest.NewRecorder()
312+
313+
status, actualErr := fileserver.ServeHTTP(responseRecorder, request)
314+
315+
// check the status
316+
if status != test.expectedStatus {
317+
t.Errorf(getTestPrefix(i)+"Expected status %d, found %d", test.expectedStatus, status)
318+
}
319+
320+
// check the error
321+
if actualErr != test.expectedErr {
322+
t.Errorf(getTestPrefix(i)+"Expected err %v, found %v", test.expectedErr, actualErr)
323+
}
324+
}
325+
}

0 commit comments

Comments
 (0)