| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
- // SPDX-License-Identifier: Apache-2.0
- package response // import "miniflux.app/v2/internal/http/response"
- import (
- "bytes"
- "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) {
- NewBuilder(w, r).Write()
- })
- handler.ServeHTTP(w, r)
- resp := w.Result()
- headers := map[string]string{
- "X-Content-Type-Options": "nosniff",
- "X-Frame-Options": "DENY",
- }
- 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) {
- NewBuilder(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) {
- NewBuilder(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) {
- NewBuilder(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 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) {
- NewBuilder(w, r).WithBodyAsBytes([]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) {
- NewBuilder(w, r).WithCaching("etag", 1*time.Minute, func(b *Builder) {
- b.WithBodyAsString("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, immutable"
- actualHeader := resp.Header.Get("Cache-Control")
- if actualHeader != expectedHeader {
- t.Fatalf(`Unexpected cache control header, got %q instead of %q`, actualHeader, expectedHeader)
- }
- if actualETag := resp.Header.Get("ETag"); actualETag != `"etag"` {
- t.Fatalf(`Unexpected etag header, got %q instead of %q`, actualETag, `"etag"`)
- }
- if resp.Header.Get("Expires") == "" {
- t.Fatalf(`Expires header should not be empty`)
- }
- }
- func TestBuildResponseWithCachingAndIfNoneMatch(t *testing.T) {
- tests := []struct {
- name string
- ifNoneMatch string
- expectedStatus int
- expectedBody string
- }{
- {"matching strong etag", `"etag"`, http.StatusNotModified, ""},
- {"matching weak etag", `W/"etag"`, http.StatusNotModified, ""},
- {"multiple etags with match", `"other", W/"etag"`, http.StatusNotModified, ""},
- {"wildcard", `*`, http.StatusNotModified, ""},
- {"non-matching etag", `"different"`, http.StatusOK, "cached body"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- r, err := http.NewRequest("GET", "/", nil)
- if err != nil {
- t.Fatal(err)
- }
- r.Header.Set("If-None-Match", tt.ifNoneMatch)
- w := httptest.NewRecorder()
- handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- NewBuilder(w, r).WithCaching("etag", 1*time.Minute, func(b *Builder) {
- b.WithBodyAsString("cached body")
- b.Write()
- })
- })
- handler.ServeHTTP(w, r)
- resp := w.Result()
- if resp.StatusCode != tt.expectedStatus {
- t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, tt.expectedStatus)
- }
- if actual := w.Body.String(); actual != tt.expectedBody {
- t.Fatalf(`Unexpected body, got %q instead of %q`, actual, tt.expectedBody)
- }
- if resp.Header.Get("Cache-Control") != "public, immutable" {
- t.Fatalf(`Unexpected Cache-Control header: %q`, resp.Header.Get("Cache-Control"))
- }
- if resp.Header.Get("Expires") == "" {
- t.Fatalf(`Expires header should not be empty`)
- }
- })
- }
- }
- func TestNormalizeETag(t *testing.T) {
- tests := []struct {
- input string
- expected string
- }{
- {"abc", `"abc"`},
- {`"already-quoted"`, `"already-quoted"`},
- {`W/"weak"`, `W/"weak"`},
- {"", ""},
- {" spaced ", `"spaced"`},
- }
- for _, tt := range tests {
- t.Run(tt.input, func(t *testing.T) {
- if actual := normalizeETag(tt.input); actual != tt.expected {
- t.Fatalf(`normalizeETag(%q) = %q, want %q`, tt.input, actual, tt.expected)
- }
- })
- }
- }
- func TestIfNoneMatch(t *testing.T) {
- tests := []struct {
- name string
- headerValue string
- etag string
- expected bool
- }{
- {"empty header", "", `"etag"`, false},
- {"empty etag", `"etag"`, "", false},
- {"exact match", `"etag"`, `"etag"`, true},
- {"weak vs strong match", `W/"etag"`, `"etag"`, true},
- {"wildcard", `*`, `"etag"`, true},
- {"no match", `"other"`, `"etag"`, false},
- {"match in list", `"a", "etag", "b"`, `"etag"`, true},
- {"no match in list", `"a", "b", "c"`, `"etag"`, false},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if actual := ifNoneMatch(tt.headerValue, tt.etag); actual != tt.expected {
- t.Fatalf(`ifNoneMatch(%q, %q) = %v, want %v`, tt.headerValue, tt.etag, actual, tt.expected)
- }
- })
- }
- }
- func TestBuildResponseWithBrotliCompression(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) {
- NewBuilder(w, r).WithBodyAsString(body).Write()
- })
- handler.ServeHTTP(w, r)
- resp := w.Result()
- expected := "br"
- actual := resp.Header.Get("Content-Encoding")
- if actual != expected {
- t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
- }
- }
- func TestBuildResponseWithGzipCompression(t *testing.T) {
- body := strings.Repeat("a", compressionThreshold+1)
- r, err := http.NewRequest("GET", "/", nil)
- r.Header.Set("Accept-Encoding", "gzip, deflate")
- if err != nil {
- t.Fatal(err)
- }
- w := httptest.NewRecorder()
- handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- NewBuilder(w, r).WithBodyAsString(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) {
- NewBuilder(w, r).WithBodyAsString(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)
- }
- expectedVary := "Accept-Encoding"
- actualVary := resp.Header.Get("Vary")
- if actualVary != expectedVary {
- t.Fatalf(`Unexpected vary header value, got %q instead of %q`, actualVary, expectedVary)
- }
- }
- 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) {
- NewBuilder(w, r).WithBodyAsString(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)
- }
- expectedVary := ""
- actualVary := resp.Header.Get("Vary")
- if actualVary != expectedVary {
- t.Fatalf(`Unexpected vary header value, got %q instead of %q`, actualVary, expectedVary)
- }
- }
- 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) {
- NewBuilder(w, r).WithBodyAsString(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)
- }
- expectedVary := ""
- actualVary := resp.Header.Get("Vary")
- if actualVary != expectedVary {
- t.Fatalf(`Unexpected vary header value, got %q instead of %q`, actualVary, expectedVary)
- }
- }
- 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) {
- NewBuilder(w, r).WithBodyAsString(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)
- }
- expectedVary := "Accept-Encoding"
- actualVary := resp.Header.Get("Vary")
- if actualVary != expectedVary {
- t.Fatalf(`Unexpected vary header value, got %q instead of %q`, actualVary, expectedVary)
- }
- }
- func TestBuildResponseWithReaderBody(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) {
- NewBuilder(w, r).WithBodyAsReader(bytes.NewBufferString("body")).Write()
- })
- handler.ServeHTTP(w, r)
- if actualBody := w.Body.String(); actualBody != "body" {
- t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, "body")
- }
- }
|