Conflict: sendfile.go (!linux build tag) was incorrectly modified to include linuxSendfile and getSocketFd functions which already exist in sendfile_linux.go. Resolution: Keep HEAD version (simple fallback returning ENOTSUP) as Linux implementation is properly separated in sendfile_linux.go. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
494 lines
12 KiB
Go
494 lines
12 KiB
Go
// Package compression 提供 gzip_static 预压缩文件功能的测试。
|
||
//
|
||
// 该文件测试 gzip_static 模块的各项功能,包括:
|
||
// - Brotli 和 Gzip 文件优先级
|
||
// - Gzip 回退机制
|
||
// - Accept-Encoding 头解析
|
||
// - 扩展名检查
|
||
// - 路径遍历防护
|
||
// - Vary 头设置
|
||
//
|
||
// 作者:xfy
|
||
package compression
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"github.com/valyala/fasthttp"
|
||
)
|
||
|
||
// TestGzipStaticServeFile_BrotliPriority 测试 .br 文件优先于 .gz 文件
|
||
func TestGzipStaticServeFile_BrotliPriority(t *testing.T) {
|
||
// 创建临时目录
|
||
tmpDir := t.TempDir()
|
||
|
||
// 创建 .br 和 .gz 文件
|
||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||
gzFile := filepath.Join(tmpDir, "test.js.gz")
|
||
|
||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||
}
|
||
if err := os.WriteFile(gzFile, []byte("gz content"), 0644); err != nil {
|
||
t.Fatalf("创建 .gz 文件失败: %v", err)
|
||
}
|
||
|
||
g := NewGzipStatic(true, tmpDir, nil)
|
||
|
||
tests := []struct {
|
||
acceptEncoding string
|
||
name string
|
||
wantEncoding string
|
||
wantServed bool
|
||
}{
|
||
{
|
||
name: "同时支持 br 和 gzip,优先返回 br",
|
||
acceptEncoding: "br, gzip",
|
||
wantServed: true,
|
||
wantEncoding: "br",
|
||
},
|
||
{
|
||
name: "只支持 br",
|
||
acceptEncoding: "br",
|
||
wantServed: true,
|
||
wantEncoding: "br",
|
||
},
|
||
{
|
||
name: "只支持 gzip",
|
||
acceptEncoding: "gzip",
|
||
wantServed: true,
|
||
wantEncoding: "gzip",
|
||
},
|
||
{
|
||
name: "支持 deflate 和 gzip,返回 gzip",
|
||
acceptEncoding: "deflate, gzip",
|
||
wantServed: true,
|
||
wantEncoding: "gzip",
|
||
},
|
||
{
|
||
name: "不支持任何编码",
|
||
acceptEncoding: "",
|
||
wantServed: false,
|
||
wantEncoding: "",
|
||
},
|
||
{
|
||
name: "支持 br(大小写不敏感)",
|
||
acceptEncoding: "BR",
|
||
wantServed: true,
|
||
wantEncoding: "br",
|
||
},
|
||
{
|
||
name: "支持 gzip(大小写不敏感)",
|
||
acceptEncoding: "GZIP",
|
||
wantServed: true,
|
||
wantEncoding: "gzip",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
|
||
|
||
served := g.ServeFile(ctx, "test.js")
|
||
|
||
if served != tt.wantServed {
|
||
t.Errorf("ServeFile() = %v, want %v", served, tt.wantServed)
|
||
}
|
||
|
||
if tt.wantServed {
|
||
encoding := ctx.Response.Header.Peek("Content-Encoding")
|
||
if string(encoding) != tt.wantEncoding {
|
||
t.Errorf("Content-Encoding = %q, want %q", string(encoding), tt.wantEncoding)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestGzipStaticServeFile_GzipFallback 测试仅 .gz 存在时的回退
|
||
func TestGzipStaticServeFile_GzipFallback(t *testing.T) {
|
||
// 创建临时目录
|
||
tmpDir := t.TempDir()
|
||
|
||
// 只创建 .gz 文件
|
||
gzFile := filepath.Join(tmpDir, "test.css.gz")
|
||
if err := os.WriteFile(gzFile, []byte("gz content"), 0644); err != nil {
|
||
t.Fatalf("创建 .gz 文件失败: %v", err)
|
||
}
|
||
|
||
g := NewGzipStatic(true, tmpDir, nil)
|
||
|
||
tests := []struct {
|
||
acceptEncoding string
|
||
name string
|
||
wantEncoding string
|
||
wantServed bool
|
||
}{
|
||
{
|
||
name: "支持 br 但没有 .br 文件,回退到 gzip",
|
||
acceptEncoding: "br, gzip",
|
||
wantServed: true,
|
||
wantEncoding: "gzip",
|
||
},
|
||
{
|
||
name: "只支持 gzip",
|
||
acceptEncoding: "gzip",
|
||
wantServed: true,
|
||
wantEncoding: "gzip",
|
||
},
|
||
{
|
||
name: "只支持 br,没有 .br 文件,不服务",
|
||
acceptEncoding: "br",
|
||
wantServed: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
|
||
|
||
served := g.ServeFile(ctx, "test.css")
|
||
|
||
if served != tt.wantServed {
|
||
t.Errorf("ServeFile() = %v, want %v", served, tt.wantServed)
|
||
}
|
||
|
||
if tt.wantServed {
|
||
encoding := ctx.Response.Header.Peek("Content-Encoding")
|
||
if string(encoding) != tt.wantEncoding {
|
||
t.Errorf("Content-Encoding = %q, want %q", string(encoding), tt.wantEncoding)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestGzipStaticServeFile_AcceptEncodingParsing 测试 Accept-Encoding 头解析
|
||
func TestGzipStaticServeFile_AcceptEncodingParsing(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
// 创建测试文件
|
||
brFile := filepath.Join(tmpDir, "test.html.br")
|
||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||
}
|
||
|
||
g := NewGzipStatic(true, tmpDir, nil)
|
||
|
||
tests := []struct {
|
||
name string
|
||
acceptEncoding string
|
||
wantSupported bool
|
||
}{
|
||
{
|
||
name: "包含 br",
|
||
acceptEncoding: "gzip, deflate, br",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "br 在中间",
|
||
acceptEncoding: "gzip, br, deflate",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "br 在最后",
|
||
acceptEncoding: "gzip, deflate,br",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "包含空格",
|
||
acceptEncoding: "gzip, br , deflate",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "q-value 支持",
|
||
acceptEncoding: "br;q=0.9, gzip;q=0.8",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "没有 br",
|
||
acceptEncoding: "gzip, deflate",
|
||
wantSupported: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.Set("Accept-Encoding", tt.acceptEncoding)
|
||
|
||
served := g.ServeFile(ctx, "test.html")
|
||
|
||
if served != tt.wantSupported {
|
||
t.Errorf("ServeFile() = %v, want %v", served, tt.wantSupported)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestGzipStaticServeFile_Disabled 测试禁用时不服务
|
||
func TestGzipStaticServeFile_Disabled(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
// 创建测试文件
|
||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||
}
|
||
|
||
// 禁用的 GzipStatic
|
||
g := NewGzipStatic(false, tmpDir, nil)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||
|
||
served := g.ServeFile(ctx, "test.js")
|
||
|
||
if served {
|
||
t.Error("禁用时不应服务文件")
|
||
}
|
||
}
|
||
|
||
// TestGzipStaticServeFile_InvalidExtension 测试无效扩展名
|
||
func TestGzipStaticServeFile_InvalidExtension(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
// 创建测试文件
|
||
brFile := filepath.Join(tmpDir, "test.exe.br")
|
||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||
}
|
||
|
||
g := NewGzipStatic(true, tmpDir, nil)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||
|
||
served := g.ServeFile(ctx, "test.exe")
|
||
|
||
if served {
|
||
t.Error("无效扩展名不应服务文件")
|
||
}
|
||
}
|
||
|
||
// TestGzipStaticServeFile_PathTraversal 测试路径遍历防护
|
||
func TestGzipStaticServeFile_PathTraversal(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
// 创建测试文件
|
||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||
}
|
||
|
||
g := NewGzipStatic(true, tmpDir, nil)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||
|
||
// 尝试路径遍历
|
||
served := g.ServeFile(ctx, "../test.js")
|
||
|
||
if served {
|
||
t.Error("路径遍历应被阻止")
|
||
}
|
||
}
|
||
|
||
// TestGzipStaticServeFile_VaryHeader 测试 Vary 头设置
|
||
func TestGzipStaticServeFile_VaryHeader(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
// 创建测试文件
|
||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||
}
|
||
|
||
g := NewGzipStatic(true, tmpDir, nil)
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||
|
||
g.ServeFile(ctx, "test.js")
|
||
|
||
vary := ctx.Response.Header.Peek("Vary")
|
||
if string(vary) != "Accept-Encoding" {
|
||
t.Errorf("Vary 头 = %q, want %q", string(vary), "Accept-Encoding")
|
||
}
|
||
}
|
||
|
||
// TestNewGzipStatic_DefaultExtensions 测试默认扩展名
|
||
func TestNewGzipStatic_DefaultExtensions(t *testing.T) {
|
||
g := NewGzipStatic(true, "/tmp", nil)
|
||
|
||
expected := []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
|
||
got := g.Extensions()
|
||
|
||
if len(got) != len(expected) {
|
||
t.Errorf("扩展名数量 = %d, want %d", len(got), len(expected))
|
||
}
|
||
|
||
for i, ext := range expected {
|
||
if got[i] != ext {
|
||
t.Errorf("扩展名[%d] = %q, want %q", i, got[i], ext)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestNewGzipStatic_CustomExtensions 测试自定义扩展名
|
||
func TestNewGzipStatic_CustomExtensions(t *testing.T) {
|
||
custom := []string{".custom", ".ext"}
|
||
g := NewGzipStatic(true, "/tmp", custom)
|
||
|
||
got := g.Extensions()
|
||
if len(got) != 2 || got[0] != ".custom" || got[1] != ".ext" {
|
||
t.Errorf("自定义扩展名 = %v, want %v", got, custom)
|
||
}
|
||
}
|
||
|
||
// TestGzipStatic_Enabled 测试 Enabled 方法
|
||
func TestGzipStatic_Enabled(t *testing.T) {
|
||
g1 := NewGzipStatic(true, "/tmp", nil)
|
||
if !g1.Enabled() {
|
||
t.Error("Enabled() = false, want true")
|
||
}
|
||
|
||
g2 := NewGzipStatic(false, "/tmp", nil)
|
||
if g2.Enabled() {
|
||
t.Error("Enabled() = true, want false")
|
||
}
|
||
}
|
||
|
||
// TestDefaultExtensions 测试默认扩展名(通过 NewGzipStatic 间接测试)
|
||
func TestDefaultExtensions(t *testing.T) {
|
||
expected := []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
|
||
g := NewGzipStatic(true, "/tmp", nil)
|
||
got := g.Extensions()
|
||
|
||
if len(got) != len(expected) {
|
||
t.Errorf("默认扩展名数量 = %d, want %d", len(got), len(expected))
|
||
}
|
||
|
||
for i, ext := range expected {
|
||
if got[i] != ext {
|
||
t.Errorf("默认扩展名[%d] = %q, want %q", i, got[i], ext)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestSupportsEncoding 测试 supportsEncoding 函数
|
||
func TestSupportsEncoding(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
acceptEncoding string
|
||
wantEncoding string
|
||
wantSupported bool
|
||
}{
|
||
{
|
||
name: "支持 br",
|
||
acceptEncoding: "br",
|
||
wantEncoding: ".br",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "支持 gzip",
|
||
acceptEncoding: "gzip",
|
||
wantEncoding: ".gz",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "不支持",
|
||
acceptEncoding: "deflate",
|
||
wantEncoding: ".br",
|
||
wantSupported: false,
|
||
},
|
||
{
|
||
name: "空 Accept-Encoding",
|
||
acceptEncoding: "",
|
||
wantEncoding: ".br",
|
||
wantSupported: false,
|
||
},
|
||
{
|
||
name: "br 在中间",
|
||
acceptEncoding: "gzip, br, deflate",
|
||
wantEncoding: ".br",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "gzip 在中间",
|
||
acceptEncoding: "br, gzip, deflate",
|
||
wantEncoding: ".gz",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "大小写不敏感",
|
||
acceptEncoding: "BR, GZIP",
|
||
wantEncoding: ".br",
|
||
wantSupported: true,
|
||
},
|
||
{
|
||
name: "未知扩展名",
|
||
acceptEncoding: "br, gzip",
|
||
wantEncoding: ".unknown",
|
||
wantSupported: false,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
supported := supportsEncoding([]byte(tt.acceptEncoding), tt.wantEncoding)
|
||
if supported != tt.wantSupported {
|
||
t.Errorf("supportsEncoding(%q, %q) = %v, want %v",
|
||
tt.acceptEncoding, tt.wantEncoding, supported, tt.wantSupported)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestTryServeFile 测试预压缩文件服务(通过 ServeFile 间接测试)
|
||
func TestTryServeFile(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
|
||
// 创建测试文件
|
||
brFile := filepath.Join(tmpDir, "test.js.br")
|
||
if err := os.WriteFile(brFile, []byte("br content"), 0644); err != nil {
|
||
t.Fatalf("创建 .br 文件失败: %v", err)
|
||
}
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.Header.Set("Accept-Encoding", "br")
|
||
|
||
g := NewGzipStatic(true, tmpDir, nil)
|
||
served := g.ServeFile(ctx, "test.js")
|
||
|
||
if !served {
|
||
t.Error("ServeFile() = false, want true")
|
||
}
|
||
|
||
encoding := ctx.Response.Header.Peek("Content-Encoding")
|
||
if string(encoding) != "br" {
|
||
t.Errorf("Content-Encoding = %q, want %q", string(encoding), "br")
|
||
}
|
||
}
|
||
|
||
// TestGzipStatic_PrecompressedExtensions 测试预压缩扩展名优先级
|
||
func TestGzipStatic_PrecompressedExtensions(t *testing.T) {
|
||
g := NewGzipStatic(true, "/tmp", nil)
|
||
|
||
// 验证默认预压缩扩展名顺序
|
||
expected := []string{".br", ".gz"}
|
||
if len(g.precompressedExtensions) != len(expected) {
|
||
t.Errorf("预压缩扩展名数量 = %d, want %d", len(g.precompressedExtensions), len(expected))
|
||
}
|
||
|
||
for i, ext := range expected {
|
||
if g.precompressedExtensions[i] != ext {
|
||
t.Errorf("预压缩扩展名[%d] = %q, want %q", i, g.precompressedExtensions[i], ext)
|
||
}
|
||
}
|
||
}
|