From 1f58b37a5e86603b16e137031c36f37580e9d410 Mon Sep 17 00:00:00 2001 From: Frédéric Guillot Date: Sun, 7 Oct 2018 18:42:43 -0700 Subject: Refactor HTTP response builder --- http/response/builder.go | 134 +++++++++++++++ http/response/builder_test.go | 351 ++++++++++++++++++++++++++++++++++++++++ http/response/doc.go | 10 ++ http/response/html/doc.go | 10 ++ http/response/html/html.go | 92 ++++++----- http/response/html/html_test.go | 212 ++++++++++++++++++++++++ http/response/json/doc.go | 10 ++ http/response/json/json.go | 127 ++++++++------- http/response/json/json_test.go | 313 +++++++++++++++++++++++++++++++++++ http/response/response.go | 63 -------- http/response/xml/doc.go | 10 ++ http/response/xml/xml.go | 25 +-- http/response/xml/xml_test.go | 83 ++++++++++ 13 files changed, 1268 insertions(+), 172 deletions(-) create mode 100644 http/response/builder.go create mode 100644 http/response/builder_test.go create mode 100644 http/response/doc.go create mode 100644 http/response/html/doc.go create mode 100644 http/response/html/html_test.go create mode 100644 http/response/json/doc.go create mode 100644 http/response/json/json_test.go delete mode 100644 http/response/response.go create mode 100644 http/response/xml/doc.go create mode 100644 http/response/xml/xml_test.go (limited to 'http') diff --git a/http/response/builder.go b/http/response/builder.go new file mode 100644 index 0000000..f17c62e --- /dev/null +++ b/http/response/builder.go @@ -0,0 +1,134 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package response // import "miniflux.app/http/response" + +import ( + "compress/flate" + "compress/gzip" + "fmt" + "net/http" + "strings" + "time" +) + +const compressionThreshold = 1024 + +// Builder generates HTTP responses. +type Builder struct { + w http.ResponseWriter + r *http.Request + statusCode int + headers map[string]string + enableCompression bool + body interface{} +} + +// WithStatus uses the given status code to build the response. +func (b *Builder) WithStatus(statusCode int) *Builder { + b.statusCode = statusCode + return b +} + +// WithHeader adds the given HTTP header to the response. +func (b *Builder) WithHeader(key, value string) *Builder { + b.headers[key] = value + return b +} + +// WithBody uses the given body to build the response. +func (b *Builder) WithBody(body interface{}) *Builder { + b.body = body + return b +} + +// WithAttachment forces the document to be downloaded by the web browser. +func (b *Builder) WithAttachment(filename string) *Builder { + b.headers["Content-Disposition"] = fmt.Sprintf("attachment; filename=%s", filename) + return b +} + +// WithoutCompression disables HTTP compression. +func (b *Builder) WithoutCompression() *Builder { + b.enableCompression = false + return b +} + +// WithCaching adds caching headers to the response. +func (b *Builder) WithCaching(etag string, duration time.Duration, callback func(*Builder)) { + b.headers["ETag"] = etag + b.headers["Cache-Control"] = "public" + b.headers["Expires"] = time.Now().Add(duration).Format(time.RFC1123) + + if etag == b.r.Header.Get("If-None-Match") { + b.statusCode = http.StatusNotModified + b.body = nil + b.Write() + } else { + callback(b) + } +} + +// Write generates the HTTP response. +func (b *Builder) Write() { + if b.body == nil { + b.writeHeaders() + return + } + + switch v := b.body.(type) { + case []byte: + b.compress(v) + case string: + b.compress([]byte(v)) + case error: + b.compress([]byte(v.Error())) + } +} + +func (b *Builder) writeHeaders() { + b.headers["X-XSS-Protection"] = "1; mode=block" + b.headers["X-Content-Type-Options"] = "nosniff" + b.headers["X-Frame-Options"] = "DENY" + b.headers["Content-Security-Policy"] = "default-src 'self'; img-src *; media-src *; frame-src *; child-src *" + + for key, value := range b.headers { + b.w.Header().Set(key, value) + } + + b.w.WriteHeader(b.statusCode) +} + +func (b *Builder) compress(data []byte) { + if b.enableCompression && len(data) > compressionThreshold { + acceptEncoding := b.r.Header.Get("Accept-Encoding") + + switch { + case strings.Contains(acceptEncoding, "gzip"): + b.headers["Content-Encoding"] = "gzip" + b.writeHeaders() + + gzipWriter := gzip.NewWriter(b.w) + defer gzipWriter.Close() + gzipWriter.Write(data) + return + case strings.Contains(acceptEncoding, "deflate"): + b.headers["Content-Encoding"] = "deflate" + b.writeHeaders() + + flateWriter, _ := flate.NewWriter(b.w, -1) + defer flateWriter.Close() + flateWriter.Write(data) + return + } + } + + b.writeHeaders() + b.w.Write(data) +} + +// New creates a new response builder. +func New(w http.ResponseWriter, r *http.Request) *Builder { + return &Builder{w: w, r: r, statusCode: http.StatusOK, headers: make(map[string]string), enableCompression: true} +} diff --git a/http/response/builder_test.go b/http/response/builder_test.go new file mode 100644 index 0000000..f4fef47 --- /dev/null +++ b/http/response/builder_test.go @@ -0,0 +1,351 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package response // import "miniflux.app/http/response" + +import ( + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestResponseHasCommonHeaders(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + headers := map[string]string{ + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + "Content-Security-Policy": "default-src 'self'; img-src *; media-src *; frame-src *; child-src *", + } + + for header, expected := range headers { + actual := resp.Header.Get(header) + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } + } +} + +func TestBuildResponseWithCustomStatusCode(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithStatus(http.StatusNotAcceptable).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNotAcceptable + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } +} + +func TestBuildResponseWithCustomHeader(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithHeader("X-My-Header", "Value").Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "Value" + actual := resp.Header.Get("X-My-Header") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithAttachment(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithAttachment("my_file.pdf").Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "attachment; filename=my_file.pdf" + actual := resp.Header.Get("Content-Disposition") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithError(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(errors.New("Some error")).Write() + }) + + handler.ServeHTTP(w, r) + + expectedBody := `Some error` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } +} + +func TestBuildResponseWithByteBody(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody([]byte("body")).Write() + }) + + handler.ServeHTTP(w, r) + + expectedBody := `body` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } +} + +func TestBuildResponseWithCachingEnabled(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithCaching("etag", 1*time.Minute, func(b *Builder) { + b.WithBody("cached body") + b.Write() + }) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `cached body` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedHeader := "public" + actualHeader := resp.Header.Get("Cache-Control") + if actualHeader != expectedHeader { + t.Fatalf(`Unexpected cache control header, got %q instead of %q`, actualHeader, expectedHeader) + } + + if resp.Header.Get("Expires") == "" { + t.Fatalf(`Expires header should not be empty`) + } +} + +func TestBuildResponseWithCachingAndEtag(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("If-None-Match", "etag") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithCaching("etag", 1*time.Minute, func(b *Builder) { + b.WithBody("cached body") + b.Write() + }) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNotModified + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedHeader := "public" + actualHeader := resp.Header.Get("Cache-Control") + if actualHeader != expectedHeader { + t.Fatalf(`Unexpected cache control header, got %q instead of %q`, actualHeader, expectedHeader) + } + + if resp.Header.Get("Expires") == "" { + t.Fatalf(`Expires header should not be empty`) + } +} + +func TestBuildResponseWithGzipCompression(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "gzip, deflate, br") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "gzip" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithDeflateCompression(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "deflate") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "deflate" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithCompressionDisabled(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "deflate") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).WithoutCompression().Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithDeflateCompressionAndSmallPayload(t *testing.T) { + body := strings.Repeat("a", compressionThreshold) + r, err := http.NewRequest("GET", "/", nil) + r.Header.Set("Accept-Encoding", "deflate") + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} + +func TestBuildResponseWithoutCompressionHeader(t *testing.T) { + body := strings.Repeat("a", compressionThreshold+1) + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + New(w, r).WithBody(body).Write() + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expected := "" + actual := resp.Header.Get("Content-Encoding") + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } +} diff --git a/http/response/doc.go b/http/response/doc.go new file mode 100644 index 0000000..007e0fa --- /dev/null +++ b/http/response/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +/* + +Package response contains everything related to HTTP responses. + +*/ +package response // import "miniflux.app/http/response" diff --git a/http/response/html/doc.go b/http/response/html/doc.go new file mode 100644 index 0000000..91d3543 --- /dev/null +++ b/http/response/html/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +/* + +Package html contains HTML response functions. + +*/ +package html // import "miniflux.app/http/response/html" diff --git a/http/response/html/html.go b/http/response/html/html.go index 65a4649..f173fdb 100644 --- a/http/response/html/html.go +++ b/http/response/html/html.go @@ -1,6 +1,6 @@ // Copyright 2018 Frédéric Guillot. All rights reserved. -// Use of this source code is governed by the Apache 2.0 -// license that can be found in the LICENSE file. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. package html // import "miniflux.app/http/response/html" @@ -11,48 +11,64 @@ import ( "miniflux.app/logger" ) -// OK writes a standard HTML response. -func OK(w http.ResponseWriter, r *http.Request, b []byte) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - response.Compress(w, r, b) +// OK creates a new HTML response with a 200 status code. +func OK(w http.ResponseWriter, r *http.Request, body interface{}) { + builder := response.New(w, r) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody(body) + builder.Write() } -// ServerError sends a 500 error to the browser. -func ServerError(w http.ResponseWriter, err error) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusInternalServerError) - - if err != nil { - logger.Error("[Internal Server Error] %v", err) - w.Write([]byte("Internal Server Error: " + err.Error())) - } else { - w.Write([]byte("Internal Server Error")) - } +// ServerError sends an internal error to the client. +func ServerError(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("[HTTP:Internal Server Error] %s => %v", r.URL, err) + + builder := response.New(w, r) + builder.WithStatus(http.StatusInternalServerError) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody(err) + builder.Write() +} + +// BadRequest sends a bad request error to the client. +func BadRequest(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("[HTTP:Bad Request] %s => %v", r.URL, err) + + builder := response.New(w, r) + builder.WithStatus(http.StatusBadRequest) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody(err) + builder.Write() } -// BadRequest sends a 400 error to the browser. -func BadRequest(w http.ResponseWriter, err error) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusBadRequest) - - if err != nil { - logger.Error("[Bad Request] %v", err) - w.Write([]byte("Bad Request: " + err.Error())) - } else { - w.Write([]byte("Bad Request")) - } +// Forbidden sends a forbidden error to the client. +func Forbidden(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Forbidden] %s", r.URL) + + builder := response.New(w, r) + builder.WithStatus(http.StatusForbidden) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody("Access Forbidden") + builder.Write() } -// NotFound sends a 404 error to the browser. -func NotFound(w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("Page Not Found")) +// NotFound sends a page not found error to the client. +func NotFound(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Not Found] %s", r.URL) + + builder := response.New(w, r) + builder.WithStatus(http.StatusNotFound) + builder.WithHeader("Content-Type", "text/html; charset=utf-8") + builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store") + builder.WithBody("Page Not Found") + builder.Write() } -// Forbidden sends a 403 error to the browser. -func Forbidden(w http.ResponseWriter) { - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.WriteHeader(http.StatusForbidden) - w.Write([]byte("Access Forbidden")) +// Redirect redirects the user to another location. +func Redirect(w http.ResponseWriter, r *http.Request, uri string) { + http.Redirect(w, r, uri, http.StatusFound) } diff --git a/http/response/html/html_test.go b/http/response/html/html_test.go new file mode 100644 index 0000000..91c2b74 --- /dev/null +++ b/http/response/html/html_test.go @@ -0,0 +1,212 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package html // import "miniflux.app/http/response/html" + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" +) + +func TestOKResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + OK(w, r, "Some HTML") + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some HTML` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + headers := map[string]string{ + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "no-cache, max-age=0, must-revalidate, no-store", + } + + for header, expected := range headers { + actual := resp.Header.Get(header) + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } + } +} + +func TestServerErrorResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ServerError(w, r, errors.New("Some error")) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusInternalServerError + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some error` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/html; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestBadRequestResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + BadRequest(w, r, errors.New("Some error")) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusBadRequest + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some error` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/html; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestForbiddenResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Forbidden(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusForbidden + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Access Forbidden` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/html; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestNotFoundResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + NotFound(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNotFound + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Page Not Found` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/html; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestRedirectResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Redirect(w, r, "/path") + }) + + handler.ServeHTTP(w, r) + + resp := w.Result() + defer resp.Body.Close() + + expectedStatusCode := http.StatusFound + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedResult := "/path" + actualResult := resp.Header.Get("Location") + if actualResult != expectedResult { + t.Fatalf(`Unexpected redirect location, got %q instead of %q`, actualResult, expectedResult) + } +} diff --git a/http/response/json/doc.go b/http/response/json/doc.go new file mode 100644 index 0000000..c2a74c1 --- /dev/null +++ b/http/response/json/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +/* + +Package json contains JSON response functions. + +*/ +package json // import "miniflux.app/http/response/json" diff --git a/http/response/json/json.go b/http/response/json/json.go index f19efb0..680a20d 100644 --- a/http/response/json/json.go +++ b/http/response/json/json.go @@ -1,6 +1,6 @@ // Copyright 2018 Frédéric Guillot. All rights reserved. -// Use of this source code is governed by the Apache 2.0 -// license that can be found in the LICENSE file. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. package json // import "miniflux.app/http/response/json" @@ -13,93 +13,98 @@ import ( "miniflux.app/logger" ) -// OK sends a JSON response with the status code 200. -func OK(w http.ResponseWriter, r *http.Request, v interface{}) { - commonHeaders(w) - response.Compress(w, r, toJSON(v)) +// OK creates a new JSON response with a 200 status code. +func OK(w http.ResponseWriter, r *http.Request, body interface{}) { + builder := response.New(w, r) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSON(body)) + builder.Write() } -// Created sends a JSON response with the status code 201. -func Created(w http.ResponseWriter, v interface{}) { - commonHeaders(w) - w.WriteHeader(http.StatusCreated) - w.Write(toJSON(v)) +// Created sends a created response to the client. +func Created(w http.ResponseWriter, r *http.Request, body interface{}) { + builder := response.New(w, r) + builder.WithStatus(http.StatusCreated) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSON(body)) + builder.Write() } -// NoContent sends a JSON response with the status code 204. -func NoContent(w http.ResponseWriter) { - commonHeaders(w) - w.WriteHeader(http.StatusNoContent) +// NoContent sends a no content response to the client. +func NoContent(w http.ResponseWriter, r *http.Request) { + builder := response.New(w, r) + builder.WithStatus(http.StatusNoContent) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.Write() } -// NotFound sends a JSON response with the status code 404. -func NotFound(w http.ResponseWriter, err error) { - logger.Error("[Not Found] %v", err) - commonHeaders(w) - w.WriteHeader(http.StatusNotFound) - w.Write(encodeError(err)) +// ServerError sends an internal error to the client. +func ServerError(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("[HTTP:Internal Server Error] %s => %v", r.URL, err) + + builder := response.New(w, r) + builder.WithStatus(http.StatusInternalServerError) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(err)) + builder.Write() } -// ServerError sends a JSON response with the status code 500. -func ServerError(w http.ResponseWriter, err error) { - logger.Error("[Internal Server Error] %v", err) - commonHeaders(w) - w.WriteHeader(http.StatusInternalServerError) +// BadRequest sends a bad request error to the client. +func BadRequest(w http.ResponseWriter, r *http.Request, err error) { + logger.Error("[HTTP:Bad Request] %s => %v", r.URL, err) - if err != nil { - w.Write(encodeError(err)) - } + builder := response.New(w, r) + builder.WithStatus(http.StatusBadRequest) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(err)) + builder.Write() } -// Forbidden sends a JSON response with the status code 403. -func Forbidden(w http.ResponseWriter) { - logger.Info("[Forbidden]") - commonHeaders(w) - w.WriteHeader(http.StatusForbidden) - w.Write(encodeError(errors.New("Access Forbidden"))) -} +// Unauthorized sends a not authorized error to the client. +func Unauthorized(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Unauthorized] %s", r.URL) -// Unauthorized sends a JSON response with the status code 401. -func Unauthorized(w http.ResponseWriter) { - commonHeaders(w) - w.WriteHeader(http.StatusUnauthorized) - w.Write(encodeError(errors.New("Access Unauthorized"))) + builder := response.New(w, r) + builder.WithStatus(http.StatusUnauthorized) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(errors.New("Access Unauthorized"))) + builder.Write() } -// BadRequest sends a JSON response with the status code 400. -func BadRequest(w http.ResponseWriter, err error) { - logger.Error("[Bad Request] %v", err) - commonHeaders(w) - w.WriteHeader(http.StatusBadRequest) +// Forbidden sends a forbidden error to the client. +func Forbidden(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Forbidden] %s", r.URL) - if err != nil { - w.Write(encodeError(err)) - } + builder := response.New(w, r) + builder.WithStatus(http.StatusForbidden) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(errors.New("Access Forbidden"))) + builder.Write() } -func commonHeaders(w http.ResponseWriter) { - w.Header().Set("Accept", "application/json") - w.Header().Set("Content-Type", "application/json; charset=utf-8") +// NotFound sends a page not found error to the client. +func NotFound(w http.ResponseWriter, r *http.Request) { + logger.Error("[HTTP:Not Found] %s", r.URL) + + builder := response.New(w, r) + builder.WithStatus(http.StatusNotFound) + builder.WithHeader("Content-Type", "application/json; charset=utf-8") + builder.WithBody(toJSONError(errors.New("Resource Not Found"))) + builder.Write() } -func encodeError(err error) []byte { +func toJSONError(err error) []byte { type errorMsg struct { ErrorMessage string `json:"error_message"` } - tmp := errorMsg{ErrorMessage: err.Error()} - data, err := json.Marshal(tmp) - if err != nil { - logger.Error("json encoding error: %v", err) - } - - return data + return toJSON(errorMsg{ErrorMessage: err.Error()}) } func toJSON(v interface{}) []byte { b, err := json.Marshal(v) if err != nil { - logger.Error("json encoding error: %v", err) + logger.Error("[HTTP:JSON] %v", err) return []byte("") } diff --git a/http/response/json/json_test.go b/http/response/json/json_test.go new file mode 100644 index 0000000..d22e468 --- /dev/null +++ b/http/response/json/json_test.go @@ -0,0 +1,313 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +package json // import "miniflux.app/http/response/json" + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" +) + +func TestOKResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + OK(w, r, map[string]string{"key": "value"}) + }) + + handler.ServeHTTP(w, r) + + resp := w.Result() + defer resp.Body.Close() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"key":"value"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %q instead of %q`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestCreatedResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Created(w, r, map[string]string{"key": "value"}) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusCreated + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"key":"value"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestNoContentResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + NoContent(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNoContent + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestServerErrorResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ServerError(w, r, errors.New("some error")) + }) + + handler.ServeHTTP(w, r) + + resp := w.Result() + defer resp.Body.Close() + + expectedStatusCode := http.StatusInternalServerError + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"some error"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %q instead of %q`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestBadRequestResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + BadRequest(w, r, errors.New("Some Error")) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusBadRequest + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"Some Error"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestUnauthorizedResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Unauthorized(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusUnauthorized + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"Access Unauthorized"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestForbiddenResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Forbidden(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusForbidden + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"Access Forbidden"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestNotFoundResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + NotFound(w, r) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusNotFound + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `{"error_message":"Resource Not Found"}` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestBuildInvalidJSONResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + OK(w, r, make(chan int)) + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "application/json; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} diff --git a/http/response/response.go b/http/response/response.go deleted file mode 100644 index 4d73171..0000000 --- a/http/response/response.go +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2018 Frédéric Guillot. All rights reserved. -// Use of this source code is governed by the Apache 2.0 -// license that can be found in the LICENSE file. - -package response // import "miniflux.app/http/response" - -import ( - "compress/flate" - "compress/gzip" - "net/http" - "strings" - "time" -) - -// Redirect redirects the user to another location. -func Redirect(w http.ResponseWriter, r *http.Request, path string) { - http.Redirect(w, r, path, http.StatusFound) -} - -// NotModified sends a response with a 304 status code. -func NotModified(w http.ResponseWriter) { - w.WriteHeader(http.StatusNotModified) -} - -// Cache returns a response with caching headers. -func Cache(w http.ResponseWriter, r *http.Request, mimeType, etag string, data []byte, duration time.Duration) { - w.Header().Set("Content-Type", mimeType) - w.Header().Set("ETag", etag) - w.Header().Set("Cache-Control", "public") - w.Header().Set("Expires", time.Now().Add(duration).Format(time.RFC1123)) - - if etag == r.Header.Get("If-None-Match") { - w.WriteHeader(http.StatusNotModified) - return - } - - switch mimeType { - case "text/javascript; charset=utf-8", "text/css; charset=utf-8": - Compress(w, r, data) - default: - w.Write(data) - } -} - -// Compress the response sent to the browser. -func Compress(w http.ResponseWriter, r *http.Request, data []byte) { - acceptEncoding := r.Header.Get("Accept-Encoding") - - switch { - case strings.Contains(acceptEncoding, "gzip"): - w.Header().Set("Content-Encoding", "gzip") - gzipWriter := gzip.NewWriter(w) - defer gzipWriter.Close() - gzipWriter.Write(data) - case strings.Contains(acceptEncoding, "deflate"): - w.Header().Set("Content-Encoding", "deflate") - flateWriter, _ := flate.NewWriter(w, -1) - defer flateWriter.Close() - flateWriter.Write(data) - default: - w.Write(data) - } -} diff --git a/http/response/xml/doc.go b/http/response/xml/doc.go new file mode 100644 index 0000000..908d2f9 --- /dev/null +++ b/http/response/xml/doc.go @@ -0,0 +1,10 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the MIT license +// that can be found in the LICENSE file. + +/* + +Package xml contains XML response functions. + +*/ +package xml // import "miniflux.app/http/response/xml" diff --git a/http/response/xml/xml.go b/http/response/xml/xml.go index bceb5f1..771a85e 100644 --- a/http/response/xml/xml.go +++ b/http/response/xml/xml.go @@ -5,19 +5,24 @@ package xml // import "miniflux.app/http/response/xml" import ( - "fmt" "net/http" + + "miniflux.app/http/response" ) -// OK sends a XML document. -func OK(w http.ResponseWriter, data string) { - w.Header().Set("Content-Type", "text/xml") - w.Write([]byte(data)) +// OK writes a standard XML response with a status 200 OK. +func OK(w http.ResponseWriter, r *http.Request, body interface{}) { + builder := response.New(w, r) + builder.WithHeader("Content-Type", "text/xml; charset=utf-8") + builder.WithBody(body) + builder.Write() } -// Attachment forces the download of a XML document. -func Attachment(w http.ResponseWriter, filename, data string) { - w.Header().Set("Content-Type", "text/xml") - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) - w.Write([]byte(data)) +// Attachment forces the XML document to be downloaded by the web browser. +func Attachment(w http.ResponseWriter, r *http.Request, filename string, body interface{}) { + builder := response.New(w, r) + builder.WithHeader("Content-Type", "text/xml; charset=utf-8") + builder.WithAttachment(filename) + builder.WithBody(body) + builder.Write() } diff --git a/http/response/xml/xml_test.go b/http/response/xml/xml_test.go new file mode 100644 index 0000000..ada6bd4 --- /dev/null +++ b/http/response/xml/xml_test.go @@ -0,0 +1,83 @@ +// Copyright 2018 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package xml // import "miniflux.app/http/response/xml" + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestOKResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + OK(w, r, "Some XML") + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some XML` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + expectedContentType := "text/xml; charset=utf-8" + actualContentType := resp.Header.Get("Content-Type") + if actualContentType != expectedContentType { + t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType) + } +} + +func TestAttachmentResponse(t *testing.T) { + r, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + w := httptest.NewRecorder() + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Attachment(w, r, "file.xml", "Some XML") + }) + + handler.ServeHTTP(w, r) + resp := w.Result() + + expectedStatusCode := http.StatusOK + if resp.StatusCode != expectedStatusCode { + t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode) + } + + expectedBody := `Some XML` + actualBody := w.Body.String() + if actualBody != expectedBody { + t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody) + } + + headers := map[string]string{ + "Content-Type": "text/xml; charset=utf-8", + "Content-Disposition": "attachment; filename=file.xml", + } + + for header, expected := range headers { + actual := resp.Header.Get(header) + if actual != expected { + t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected) + } + } +} -- cgit v1.2.3