From 50f91a4d9c7e802c315ab189f79a2f1fd8cb91dc Mon Sep 17 00:00:00 2001 From: Caleb Woodbine Date: Tue, 19 Dec 2023 13:30:50 +1300 Subject: [PATCH] feat: add unit tests to handlers - adds unit tests to handlers - update to respond 404 on file not found - update method of vuejs handler response --- .gitlab-ci.yml | 6 + pkg/handlers/handlers.go | 19 +- pkg/handlers/handlers_test.go | 803 ++++++++++++++++++++++++++++++++++ 3 files changed, 823 insertions(+), 5 deletions(-) create mode 100644 pkg/handlers/handlers_test.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 74360fe..b499fc2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -88,6 +88,12 @@ lint_backend: script: - golint -set_exit_status ./... +gotest: + stage: lint + image: $IMAGE_GOLANG_ALPINE + script: + - go test -cover -v ./... + govet: stage: lint image: $IMAGE_GOLANG_ALPINE diff --git a/pkg/handlers/handlers.go b/pkg/handlers/handlers.go index e4331c1..400b624 100644 --- a/pkg/handlers/handlers.go +++ b/pkg/handlers/handlers.go @@ -1,6 +1,8 @@ package handlers import ( + "bytes" + "fmt" "html/template" "log" "net/http" @@ -60,12 +62,17 @@ func (h *Handler) serveHandlerVuejsHistoryMode() http.Handler { indexPath := path.Join(h.ServeFolder, "/index.html") tmpl, err := template.ParseFiles(indexPath) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + log.Println("warning: unable to parse template html:", err) + http.Error(w, "500 internal error", http.StatusInternalServerError) return } - if err := tmpl.Execute(w, h.TemplateMap); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + var buf bytes.Buffer + defer buf.Reset() + if err := tmpl.ExecuteTemplate(&buf, tmpl.Name(), h.TemplateMap); err != nil { + log.Println("warning: unable to execute template html:", err) + http.Error(w, "500 internal error", http.StatusInternalServerError) } + fmt.Fprint(w, &buf) }) } @@ -86,8 +93,9 @@ func (h *Handler) serveHandlerStandard() http.Handler { } } if _, err := os.Stat(path.Join(h.ServeFolder, req.URL.Path)); err != nil || isDisallowed { - req.URL.Path = h.Error404FilePath - req.RequestURI = req.URL.Path + w.WriteHeader(http.StatusNotFound) + http.ServeFile(w, req, path.Join(h.ServeFolder, h.Error404FilePath)) + return } handler.ServeHTTP(w, req) }) @@ -116,6 +124,7 @@ func (h *Handler) ServeStandardRedirect(from string, to string) http.HandlerFunc toURL, err := url.Parse(to) if err != nil { log.Printf("Unable to parse redirection destination URL '%v' for route '%v'\n", to, from) + http.Error(w, "fatal: unable to redirect to destination URL", http.StatusInternalServerError) return } toURL.RawQuery = req.URL.Query().Encode() diff --git a/pkg/handlers/handlers_test.go b/pkg/handlers/handlers_test.go new file mode 100644 index 0000000..47fc0f1 --- /dev/null +++ b/pkg/handlers/handlers_test.go @@ -0,0 +1,803 @@ +package handlers + +import ( + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" +) + +func TestHandler_serveHandlerVuejsHistoryMode(t *testing.T) { + type fields struct { + Error404FilePath string + HeaderMap map[string][]string + GzipEnabled bool + HeaderMapEnabled bool + TemplateMap map[string]string + TemplateMapEnabled bool + VueJSHistoryMode bool + ServeFolder string + } + tests := []struct { + name string + fields fields + serveFolder []struct { + name string + content string + } + tests []struct { + path string + content string + statusCode int + headers map[string]string + } + }{ + struct { + name string + fields fields + serveFolder []struct { + name string + content string + } + tests []struct { + path string + content string + statusCode int + headers map[string]string + } + }{ + name: "basic test", + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

hello world

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: "

hello world

", + statusCode: http.StatusOK, + }, + { + path: "/aaa", + content: "

hello world

", + statusCode: http.StatusOK, + }, + { + path: "/bbbb", + content: "

hello world

", + statusCode: http.StatusOK, + }, + { + path: "/index.html", + content: "

hello world

", + statusCode: http.StatusOK, + }, + }, + }, + struct { + name string + fields fields + serveFolder []struct { + name string + content string + } + tests []struct { + path string + content string + statusCode int + headers map[string]string + } + }{ + name: "templating test", + fields: fields{ + TemplateMap: map[string]string{ + "Message": "some-kinda-test", + }, + }, + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

{{.Message}}

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: "

some-kinda-test

", + statusCode: http.StatusOK, + }, + }, + }, + struct { + name string + fields fields + serveFolder []struct { + name string + content string + } + tests []struct { + path string + content string + statusCode int + headers map[string]string + } + }{ + name: "templating test with bad template 1", + fields: fields{ + TemplateMap: map[string]string{ + "Message": "some-kinda-test", + }, + }, + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

{{.Message}

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: `500 internal error +`, + statusCode: http.StatusInternalServerError, + }, + }, + }, + struct { + name string + fields fields + serveFolder []struct { + name string + content string + } + tests []struct { + path string + content string + statusCode int + headers map[string]string + } + }{ + name: "templating test with bad template 2", + fields: fields{ + TemplateMap: map[string]string{ + "Message": "some-kinda-test", + }, + }, + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

{{nil}}

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: `500 internal error +

`, + statusCode: http.StatusInternalServerError, + }, + }, + }, + struct { + name string + fields fields + serveFolder []struct { + name string + content string + } + tests []struct { + path string + content string + statusCode int + headers map[string]string + } + }{ + name: "header map test", + fields: fields{ + HeaderMapEnabled: true, + HeaderMap: map[string][]string{ + "X-Some-Header": []string{"Yes"}, + }, + }, + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

hello world

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: "

hello world

", + statusCode: http.StatusOK, + headers: map[string]string{ + "X-Some-Header": "Yes", + }, + }, + }, + }, + struct { + name string + fields fields + serveFolder []struct { + name string + content string + } + tests []struct { + path string + content string + statusCode int + headers map[string]string + } + }{ + name: "basic test with disallowed path", + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

hello world

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: "

hello world

", + statusCode: http.StatusOK, + }, + { + path: "/.ghs.yaml", + content: "

hello world

", + statusCode: http.StatusOK, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + h := &Handler{ + Error404FilePath: tt.fields.Error404FilePath, + HeaderMap: tt.fields.HeaderMap, + GzipEnabled: tt.fields.GzipEnabled, + HeaderMapEnabled: tt.fields.HeaderMapEnabled, + TemplateMap: tt.fields.TemplateMap, + TemplateMapEnabled: true, + VueJSHistoryMode: true, + ServeFolder: tt.fields.ServeFolder, + } + dir, err := os.MkdirTemp("", "serveFolder") + if err != nil { + t.Fatal(err) + } + h.ServeFolder = dir + defer os.RemoveAll(dir) + + for _, f := range tt.serveFolder { + file := filepath.Join(dir, f.name) + if err := os.WriteFile(file, []byte(f.content), 0644); err != nil { + t.Fatal(err) + } + } + + for _, te := range tt.tests { + srv := httptest.NewServer(h.serveHandlerVuejsHistoryMode()) + defer srv.Close() + tp, err := url.JoinPath(srv.URL, te.path) + if err != nil { + t.Fatal(err) + } + client := srv.Client() + resp, err := client.Get(tp) + if err != nil { + t.Fatal(err) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if got, want := resp.StatusCode, te.statusCode; got != want { + t.Errorf("Handler.serveHandlerVuejsHistoryMode() = %v, want %v for path %v", got, want, te.path) + } + if got, want := string(b), te.content; got != want { + t.Errorf("Handler.serveHandlerVuejsHistoryMode() = %v, want %v for path %v", got, want, te.path) + } + for hk, hv := range te.headers { + if got, want := resp.Header.Get(hk), hv; got != want { + t.Errorf("Handler.serveHandlerStandard() = %v, want %v for path %v", got, want, te.path) + } + } + } + }) + } +} + +func TestHandler_serveHandlerStandard(t *testing.T) { + type fields struct { + Error404FilePath string + HeaderMap map[string][]string + GzipEnabled bool + HeaderMapEnabled bool + TemplateMap map[string]string + TemplateMapEnabled bool + VueJSHistoryMode bool + ServeFolder string + } + tests := []struct { + name string + fields fields + serveFolder []struct { + name string + content string + } + tests []struct { + path string + content string + statusCode int + headers map[string]string + } + }{ + { + name: "basic test", + fields: fields{ + Error404FilePath: "404.html", + }, + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

hello world

", + }, + { + name: "404.html", + content: "

unknown page

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: "

hello world

", + statusCode: http.StatusOK, + }, + { + path: "/aaaaa", + content: `

unknown page

`, + statusCode: http.StatusNotFound, + }, + { + path: "/MMMMM.html", + content: `

unknown page

`, + statusCode: http.StatusNotFound, + }, + { + path: "/index.html", + content: "

hello world

", + statusCode: http.StatusOK, + }, + }, + }, + { + name: "basic test missing 404 page", + fields: fields{ + Error404FilePath: "404.html", + }, + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

hello world

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: "

hello world

", + statusCode: http.StatusOK, + }, + { + path: "/aaaaa", + content: `404 page not found +`, + statusCode: http.StatusNotFound, + }, + { + path: "/MMMMM.html", + content: `404 page not found +`, + statusCode: http.StatusNotFound, + }, + { + path: "/index.html", + content: "

hello world

", + statusCode: http.StatusOK, + }, + }, + }, + { + name: "header map test", + fields: fields{ + HeaderMapEnabled: true, + HeaderMap: map[string][]string{ + "X-Some-Header": []string{"Yes"}, + }, + }, + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

hello world

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: "

hello world

", + statusCode: http.StatusOK, + headers: map[string]string{ + "X-Some-Header": "Yes", + }, + }, + }, + }, + { + name: "basic test with disallowed paths", + fields: fields{ + Error404FilePath: "404.html", + }, + serveFolder: []struct { + name string + content string + }{ + { + name: "index.html", + content: "

hello world

", + }, + { + name: "404.html", + content: "

unknown page

", + }, + }, + tests: []struct { + path string + content string + statusCode int + headers map[string]string + }{ + { + path: "/", + content: "

hello world

", + statusCode: http.StatusOK, + }, + { + path: "/.ghs.yaml", + content: `

unknown page

`, + statusCode: http.StatusNotFound, + }, + { + path: "/MMMMM.html", + content: `

unknown page

`, + statusCode: http.StatusNotFound, + }, + { + path: "/index.html", + content: "

hello world

", + statusCode: http.StatusOK, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + h := &Handler{ + Error404FilePath: tt.fields.Error404FilePath, + HeaderMap: tt.fields.HeaderMap, + GzipEnabled: tt.fields.GzipEnabled, + HeaderMapEnabled: tt.fields.HeaderMapEnabled, + TemplateMap: tt.fields.TemplateMap, + TemplateMapEnabled: tt.fields.TemplateMapEnabled, + VueJSHistoryMode: tt.fields.VueJSHistoryMode, + ServeFolder: tt.fields.ServeFolder, + } + dir, err := os.MkdirTemp("", "serveFolder") + if err != nil { + t.Fatal(err) + } + h.ServeFolder = dir + defer os.RemoveAll(dir) + + for _, f := range tt.serveFolder { + file := filepath.Join(dir, f.name) + if err := os.WriteFile(file, []byte(f.content), 0644); err != nil { + t.Fatal(err) + } + } + + for _, te := range tt.tests { + srv := httptest.NewServer(h.serveHandlerStandard()) + defer srv.Close() + tp, err := url.JoinPath(srv.URL, te.path) + if err != nil { + t.Fatal(err) + } + client := srv.Client() + resp, err := client.Get(tp) + if err != nil { + t.Fatal(err) + } + b, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if got, want := resp.StatusCode, te.statusCode; got != want { + t.Errorf("Handler.serveHandlerStandard() = %v, want %v for path %v", got, want, te.path) + } + if got, want := string(b), te.content; got != want { + t.Errorf("Handler.serveHandlerStandard() = %v, want %v for path %v", got, want, te.path) + } + for hk, hv := range te.headers { + if got, want := resp.Header.Get(hk), hv; got != want { + t.Errorf("Handler.serveHandlerStandard() = %v, want %v for path %v", got, want, te.path) + } + } + } + }) + } +} + +func TestHandler_ServeHandler(t *testing.T) { + type fields struct { + Error404FilePath string + HeaderMap map[string][]string + GzipEnabled bool + HeaderMapEnabled bool + TemplateMap map[string]string + TemplateMapEnabled bool + VueJSHistoryMode bool + ServeFolder string + } + tests := []struct { + name string + fields fields + wantHandler http.Handler + }{ + { + name: "handler default", + }, + { + name: "handler vuejs history mode", + fields: fields{ + VueJSHistoryMode: true, + }, + }, + { + name: "handler default with gzip", + fields: fields{ + GzipEnabled: true, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + h := &Handler{ + Error404FilePath: tt.fields.Error404FilePath, + HeaderMap: tt.fields.HeaderMap, + GzipEnabled: tt.fields.GzipEnabled, + HeaderMapEnabled: tt.fields.HeaderMapEnabled, + TemplateMap: tt.fields.TemplateMap, + TemplateMapEnabled: tt.fields.TemplateMapEnabled, + VueJSHistoryMode: tt.fields.VueJSHistoryMode, + ServeFolder: tt.fields.ServeFolder, + } + // TODO test this better + if gotHandler := h.ServeHandler(); gotHandler == nil { + t.Errorf("Handler.ServeHandler() = %v, want not nil", gotHandler) + } + }) + } +} + +func TestHandler_ServeStandardRedirect(t *testing.T) { + type fields struct { + Error404FilePath string + HeaderMap map[string][]string + GzipEnabled bool + HeaderMapEnabled bool + TemplateMap map[string]string + TemplateMapEnabled bool + VueJSHistoryMode bool + ServeFolder string + } + type args struct { + from string + to string + } + tests := []struct { + name string + fields fields + args args + want struct { + location string + statusCode int + errorBody string + } + }{ + struct { + name string + fields fields + args args + want struct { + location string + statusCode int + errorBody string + } + }{ + name: "basic path redirect", + args: args{ + from: "/a", + to: "/b", + }, + want: struct { + location string + statusCode int + errorBody string + }{ + location: "/b", + }, + }, + struct { + name string + fields fields + args args + want struct { + location string + statusCode int + errorBody string + } + }{ + name: "basic path redirect to url", + args: args{ + from: "/a", + to: "http://example.com/", + }, + want: struct { + location string + statusCode int + errorBody string + }{ + location: "http://example.com/", + }, + }, + struct { + name string + fields fields + args args + want struct { + location string + statusCode int + errorBody string + } + }{ + name: "bad url", + args: args{ + from: "/a", + to: ":90%?88**", + }, + want: struct { + location string + statusCode int + errorBody string + }{ + errorBody: "fatal: unable to redirect to destination URL", + statusCode: http.StatusInternalServerError, + }, + }, + } + for _, tt := range tests { + tt := tt + if tt.want.statusCode == 0 { + tt.want.statusCode = http.StatusTemporaryRedirect + } + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + h := &Handler{ + Error404FilePath: tt.fields.Error404FilePath, + HeaderMap: tt.fields.HeaderMap, + GzipEnabled: tt.fields.GzipEnabled, + HeaderMapEnabled: tt.fields.HeaderMapEnabled, + TemplateMap: tt.fields.TemplateMap, + TemplateMapEnabled: tt.fields.TemplateMapEnabled, + VueJSHistoryMode: tt.fields.VueJSHistoryMode, + ServeFolder: tt.fields.ServeFolder, + } + req := httptest.NewRequest("GET", "http://example.com", nil) + w := httptest.NewRecorder() + h.ServeStandardRedirect(tt.args.from, tt.args.to)(w, req) + + // if tt.error != "" + if code := w.Result().StatusCode; code != tt.want.statusCode { + t.Errorf("Handler.ServeStandardRedirect() = %v, want %v", code, http.StatusTemporaryRedirect) + } + if got := w.Result().Header.Get("Location"); got != tt.want.location { + t.Errorf("Handler.ServeStandardRedirect() = %v, want %v", got, tt.want) + } + }) + } +}