mirror of
https://github.com/caddyserver/caddy.git
synced 2026-06-04 13:59:40 -04:00
* fix(encode): prioritize zstd and br over gzip in content negotiation * test(encode): update unit tests to reflect new default priority ties * fix(encode): move default preferences to dynamic encode handler and restore generic negotiation helper * test(encode): call real Provision function in served-response test * test(encode): rename served-response test to TestServeHTTPDefaultEncodingPreference * refactor(encode): use slices.SortStableFunc and httptest.NewRecorder as recommended * refactor(encode): simplify sorting with cmp.Compare and check request error in test * test(encode): fix variable redeclaration in TestServeHTTPDefaultEncodingPreference Fix 'no new variables on left side of :=' error by changing 'err :=' to 'err =' on line 347, since err was already declared on line 332. This fixes the build failure in the encode module tests.
373 lines
9.9 KiB
Go
373 lines
9.9 KiB
Go
package encode
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"slices"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
func BenchmarkOpenResponseWriter(b *testing.B) {
|
|
enc := new(Encode)
|
|
for b.Loop() {
|
|
enc.openResponseWriter("test", nil, false)
|
|
}
|
|
}
|
|
|
|
func TestPreferOrder(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
accept string
|
|
prefer []string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "PreferOrder(): 4 accept, 3 prefer",
|
|
accept: "deflate, gzip, br, zstd",
|
|
prefer: []string{"zstd", "br", "gzip"},
|
|
expected: []string{"zstd", "br", "gzip", "deflate"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): 2 accept, 3 prefer",
|
|
accept: "deflate, zstd",
|
|
prefer: []string{"zstd", "br", "gzip"},
|
|
expected: []string{"zstd", "deflate"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): 2 accept (1 empty), 3 prefer",
|
|
accept: "gzip,,zstd",
|
|
prefer: []string{"zstd", "br", "gzip"},
|
|
expected: []string{"zstd", "gzip", ""},
|
|
},
|
|
{
|
|
name: "PreferOrder(): 1 accept, 2 prefer",
|
|
accept: "gzip",
|
|
prefer: []string{"zstd", "gzip"},
|
|
expected: []string{"gzip"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): 4 accept (1 duplicate), 1 prefer",
|
|
accept: "deflate, gzip, br, br",
|
|
prefer: []string{"br"},
|
|
expected: []string{"br", "br", "deflate", "gzip"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): empty accept, 0 prefer",
|
|
accept: "",
|
|
prefer: []string{},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "PreferOrder(): empty accept, 1 prefer",
|
|
accept: "",
|
|
prefer: []string{"gzip"},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "PreferOrder(): with q-factor",
|
|
accept: "deflate;q=0.8, gzip;q=0.4, br;q=0.2, zstd",
|
|
prefer: []string{"gzip"},
|
|
expected: []string{"zstd", "deflate", "gzip", "br"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): with q-factor, no prefer",
|
|
accept: "deflate;q=0.8, gzip;q=0.4, br;q=0.2, zstd",
|
|
prefer: []string{},
|
|
expected: []string{"zstd", "deflate", "gzip", "br"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): q-factor=0 filtered out",
|
|
accept: "deflate;q=0.1, gzip;q=0.4, br;q=0.5, zstd;q=0",
|
|
prefer: []string{"gzip"},
|
|
expected: []string{"br", "gzip", "deflate"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): q-factor=0 filtered out, no prefer",
|
|
accept: "deflate;q=0.1, gzip;q=0.4, br;q=0.5, zstd;q=0",
|
|
prefer: []string{},
|
|
expected: []string{"br", "gzip", "deflate"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): with invalid q-factor",
|
|
accept: "br, deflate, gzip;q=2, zstd;q=0.1",
|
|
prefer: []string{"zstd", "gzip"},
|
|
expected: []string{"gzip", "br", "deflate", "zstd"},
|
|
},
|
|
{
|
|
name: "PreferOrder(): with invalid q-factor, no prefer",
|
|
accept: "br, deflate, gzip;q=2, zstd;q=0.1",
|
|
prefer: []string{},
|
|
expected: []string{"br", "deflate", "gzip", "zstd"},
|
|
},
|
|
}
|
|
|
|
enc := new(Encode)
|
|
r, _ := http.NewRequest("", "", nil)
|
|
|
|
for _, test := range testCases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if test.accept == "" {
|
|
r.Header.Del("Accept-Encoding")
|
|
} else {
|
|
r.Header.Set("Accept-Encoding", test.accept)
|
|
}
|
|
enc.Prefer = test.prefer
|
|
result := AcceptedEncodings(r, enc.Prefer)
|
|
if !slices.Equal(result, test.expected) {
|
|
t.Errorf("AcceptedEncodings() actual: %s expected: %s",
|
|
result,
|
|
test.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidate(t *testing.T) {
|
|
type testCase struct {
|
|
name string
|
|
prefer []string
|
|
wantErr bool
|
|
}
|
|
|
|
var err error
|
|
var testCases []testCase
|
|
enc := new(Encode)
|
|
|
|
enc.writerPools = map[string]*sync.Pool{
|
|
"zstd": nil,
|
|
"gzip": nil,
|
|
"br": nil,
|
|
}
|
|
testCases = []testCase{
|
|
{
|
|
name: "ValidatePrefer (zstd, gzip & br enabled): valid order with all encoder",
|
|
prefer: []string{"zstd", "br", "gzip"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd, gzip & br enabled): valid order with 2 out of 3 encoders",
|
|
prefer: []string{"br", "gzip"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd, gzip & br enabled): valid order with 1 out of 3 encoders",
|
|
prefer: []string{"gzip"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd, gzip & br enabled): 1 duplicated (once) encoder",
|
|
prefer: []string{"gzip", "zstd", "gzip"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd, gzip & br enabled): 1 not enabled encoder in prefer list",
|
|
prefer: []string{"br", "zstd", "gzip", "deflate"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd, gzip & br enabled): no prefer list",
|
|
prefer: []string{},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
enc.Prefer = test.prefer
|
|
err = enc.Validate()
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr)
|
|
}
|
|
})
|
|
}
|
|
|
|
enc.writerPools = map[string]*sync.Pool{
|
|
"zstd": nil,
|
|
"gzip": nil,
|
|
}
|
|
testCases = []testCase{
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): 1 not enabled encoder in prefer list",
|
|
prefer: []string{"zstd", "br", "gzip"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): 2 not enabled encoder in prefer list",
|
|
prefer: []string{"br", "zstd", "gzip", "deflate"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): only not enabled encoder in prefer list",
|
|
prefer: []string{"deflate", "br", "gzip"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): 1 duplicated (once) encoder in prefer list",
|
|
prefer: []string{"gzip", "zstd", "gzip"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): 1 duplicated (twice) encoder in prefer list",
|
|
prefer: []string{"gzip", "zstd", "gzip", "gzip"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): 1 duplicated encoder in prefer list",
|
|
prefer: []string{"zstd", "zstd", "gzip", "gzip"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): 1 duplicated not enabled encoder in prefer list",
|
|
prefer: []string{"br", "br", "gzip"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): 2 duplicated not enabled encoder in prefer list",
|
|
prefer: []string{"br", "deflate", "br", "deflate"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): valid order zstd first",
|
|
prefer: []string{"zstd", "gzip"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "ValidatePrefer (zstd & gzip enabled): valid order gzip first",
|
|
prefer: []string{"gzip", "zstd"},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
enc.Prefer = test.prefer
|
|
err = enc.Validate()
|
|
if (err != nil) != test.wantErr {
|
|
t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsEncodeAllowed(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
headers http.Header
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "Without any headers",
|
|
headers: http.Header{},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Without Cache-Control HTTP header",
|
|
headers: http.Header{
|
|
"Accept-Encoding": {"gzip"},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "Cache-Control HTTP header ending with no-transform directive",
|
|
headers: http.Header{
|
|
"Accept-Encoding": {"gzip"},
|
|
"Cache-Control": {"no-cache; no-transform"},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "With Cache-Control HTTP header no-transform as Cache-Extension value",
|
|
headers: http.Header{
|
|
"Accept-Encoding": {"gzip"},
|
|
"Cache-Control": {`no-store; no-cache; community="no-transform"`},
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, test := range testCases {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if result := isEncodeAllowed(test.headers); result != test.expected {
|
|
t.Errorf("The headers given to the isEncodeAllowed should return %t, %t given.",
|
|
result,
|
|
test.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type mockEncoder struct{}
|
|
|
|
func (mockEncoder) Write(p []byte) (n int, err error) { return len(p), nil }
|
|
func (mockEncoder) Close() error { return nil }
|
|
func (mockEncoder) Reset(w io.Writer) {}
|
|
func (mockEncoder) Flush() error { return nil }
|
|
|
|
func TestServeHTTPDefaultEncodingPreference(t *testing.T) {
|
|
enc := new(Encode)
|
|
enc.MinLength = 1 // compress everything
|
|
enc.writerPools = map[string]*sync.Pool{
|
|
"gzip": {
|
|
New: func() any { return mockEncoder{} },
|
|
},
|
|
"zstd": {
|
|
New: func() any { return mockEncoder{} },
|
|
},
|
|
}
|
|
|
|
// Call Provision() with a valid caddy.Context to exercise the real path
|
|
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
defer cancel()
|
|
if err := enc.Provision(ctx); err != nil {
|
|
t.Fatalf("Provision failed: %v", err)
|
|
}
|
|
|
|
// Test default preference: zstd preferred over gzip
|
|
r, err := http.NewRequest("GET", "/", nil)
|
|
if err != nil {
|
|
t.Fatalf("error creating request: %v", err)
|
|
}
|
|
r.Header.Set("Accept-Encoding", "gzip, deflate, br, zstd")
|
|
|
|
w := httptest.NewRecorder()
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
|
next := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, err := w.Write([]byte("Hello, world! This is a long enough string to satisfy min length if it wasn't 1."))
|
|
return err
|
|
})
|
|
|
|
err = enc.ServeHTTP(w, r, next)
|
|
if err != nil {
|
|
t.Fatalf("ServeHTTP returned error: %v", err)
|
|
}
|
|
|
|
// ETag suffix or Content-Encoding header should reflect zstd
|
|
contentEncoding := w.Header().Get("Content-Encoding")
|
|
if contentEncoding != "zstd" {
|
|
t.Errorf("Expected Content-Encoding to be 'zstd' by default, got '%s'", contentEncoding)
|
|
}
|
|
|
|
// Test explicit user preference: gzip over zstd
|
|
enc.Prefer = []string{"gzip", "zstd"}
|
|
|
|
w2 := httptest.NewRecorder()
|
|
w2.Header().Set("Content-Type", "text/plain")
|
|
err = enc.ServeHTTP(w2, r, next)
|
|
if err != nil {
|
|
t.Fatalf("ServeHTTP returned error: %v", err)
|
|
}
|
|
|
|
contentEncoding2 := w2.Header().Get("Content-Encoding")
|
|
if contentEncoding2 != "gzip" {
|
|
t.Errorf("Expected Content-Encoding to be 'gzip' when explicitly preferred, got '%s'", contentEncoding2)
|
|
}
|
|
}
|