Phase 1: 单元测试补充 - 新增 config/loader_test.go,覆盖配置加载、include 合并、循环检测 - 补充 cache/cache_test.go,测试 RefreshCachedAt、DeleteByPatternWithMethod - 补充 handler/static_test.go,测试 SetExpires、setCacheHeaders、parseExpires Phase 2: E2E 测试扩展 - 新增 ratelimit_e2e_test.go,测试请求限流功能 - 新增 compression_e2e_test.go,测试 Gzip 压缩功能 - 新增 access_e2e_test.go,测试 IP 访问控制 - 新增 rewrite_e2e_test.go,测试 URL 重写和重定向 覆盖率提升: 82.3% -> 83.1% E2E 测试用例: ~84 -> ~104 (+20) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
297 lines
8.5 KiB
Go
297 lines
8.5 KiB
Go
//go:build e2e
|
||
|
||
// rewrite_e2e_test.go - URL 重写 E2E 测试(L3 层,需要 Docker)
|
||
//
|
||
// 测试 lolly URL 重写功能,包括:
|
||
// - URL 重写
|
||
// - 正则表达式重写
|
||
// - 重定向 (301/302)
|
||
// - 内部重定向
|
||
//
|
||
// 作者:xfy
|
||
package e2e
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
|
||
"rua.plus/lolly/internal/e2e/testutil"
|
||
)
|
||
|
||
// TestE2ERewriteBasic 测试基本 URL 重写。
|
||
func TestE2ERewriteBasic(t *testing.T) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||
defer cancel()
|
||
|
||
if !testutil.LollyImageAvailable(ctx) {
|
||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||
}
|
||
|
||
// 配置 URL 重写:/old/* -> /new/*
|
||
config := `
|
||
servers:
|
||
- listen: ":8080"
|
||
static:
|
||
- path: "/"
|
||
root: "/var/www/html"
|
||
rewrite:
|
||
- pattern: "^/old/(.*)$"
|
||
replacement: "/new/$1"
|
||
flag: "last"
|
||
`
|
||
|
||
// 启动 lolly
|
||
lolly, err := testutil.StartLollyContainer(ctx, "", testutil.WithConfigYAML(config))
|
||
require.NoError(t, err, "Failed to start lolly container")
|
||
defer lolly.Terminate(ctx)
|
||
|
||
err = lolly.WaitForHealthy(ctx, 30*time.Second)
|
||
require.NoError(t, err, "Lolly not healthy")
|
||
|
||
client := &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||
return http.ErrUseLastResponse // 不自动跟随重定向
|
||
}}
|
||
|
||
// 请求 /old/test 应该被重写到 /new/test
|
||
resp, err := client.Get(lolly.HTTPBaseURL() + "/old/test")
|
||
require.NoError(t, err)
|
||
defer resp.Body.Close()
|
||
|
||
t.Logf("Status: %d", resp.StatusCode)
|
||
// 重写后可能返回 404(文件不存在),但不应该返回重定向
|
||
assert.NotEqual(t, http.StatusMovedPermanently, resp.StatusCode)
|
||
assert.NotEqual(t, http.StatusFound, resp.StatusCode)
|
||
}
|
||
|
||
// TestE2ERewriteRedirect 测试重写为重定向。
|
||
func TestE2ERewriteRedirect(t *testing.T) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||
defer cancel()
|
||
|
||
if !testutil.LollyImageAvailable(ctx) {
|
||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||
}
|
||
|
||
// 配置重写为 302 重定向
|
||
config := `
|
||
servers:
|
||
- listen: ":8080"
|
||
static:
|
||
- path: "/"
|
||
root: "/var/www/html"
|
||
rewrite:
|
||
- pattern: "^/old/(.*)$"
|
||
replacement: "/new/$1"
|
||
flag: "redirect"
|
||
`
|
||
|
||
// 启动 lolly
|
||
lolly, err := testutil.StartLollyContainer(ctx, "", testutil.WithConfigYAML(config))
|
||
require.NoError(t, err, "Failed to start lolly container")
|
||
defer lolly.Terminate(ctx)
|
||
|
||
err = lolly.WaitForHealthy(ctx, 30*time.Second)
|
||
require.NoError(t, err, "Lolly not healthy")
|
||
|
||
client := &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||
return http.ErrUseLastResponse
|
||
}}
|
||
|
||
// 请求 /old/test 应该返回 302 重定向
|
||
resp, err := client.Get(lolly.HTTPBaseURL() + "/old/test")
|
||
require.NoError(t, err)
|
||
defer resp.Body.Close()
|
||
|
||
assert.Equal(t, http.StatusFound, resp.StatusCode, "Should return 302 redirect")
|
||
|
||
location := resp.Header.Get("Location")
|
||
assert.Contains(t, location, "/new/test", "Location should contain /new/test")
|
||
t.Logf("Location: %s", location)
|
||
}
|
||
|
||
// TestE2ERewritePermanent 测试永久重定向。
|
||
func TestE2ERewritePermanent(t *testing.T) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||
defer cancel()
|
||
|
||
if !testutil.LollyImageAvailable(ctx) {
|
||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||
}
|
||
|
||
// 配置永久重定向
|
||
config := `
|
||
servers:
|
||
- listen: ":8080"
|
||
static:
|
||
- path: "/"
|
||
root: "/var/www/html"
|
||
rewrite:
|
||
- pattern: "^/old/(.*)$"
|
||
replacement: "/new/$1"
|
||
flag: "permanent"
|
||
`
|
||
|
||
// 启动 lolly
|
||
lolly, err := testutil.StartLollyContainer(ctx, "", testutil.WithConfigYAML(config))
|
||
require.NoError(t, err, "Failed to start lolly container")
|
||
defer lolly.Terminate(ctx)
|
||
|
||
err = lolly.WaitForHealthy(ctx, 30*time.Second)
|
||
require.NoError(t, err, "Lolly not healthy")
|
||
|
||
client := &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||
return http.ErrUseLastResponse
|
||
}}
|
||
|
||
// 请求 /old/test 应该返回 301 永久重定向
|
||
resp, err := client.Get(lolly.HTTPBaseURL() + "/old/test")
|
||
require.NoError(t, err)
|
||
defer resp.Body.Close()
|
||
|
||
assert.Equal(t, http.StatusMovedPermanently, resp.StatusCode, "Should return 301 permanent redirect")
|
||
}
|
||
|
||
// TestE2ERewriteRegexCapture 测试正则表达式捕获组。
|
||
func TestE2ERewriteRegexCapture(t *testing.T) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||
defer cancel()
|
||
|
||
if !testutil.LollyImageAvailable(ctx) {
|
||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||
}
|
||
|
||
// 配置复杂的正则重写
|
||
config := `
|
||
servers:
|
||
- listen: ":8080"
|
||
static:
|
||
- path: "/"
|
||
root: "/var/www/html"
|
||
rewrite:
|
||
- pattern: "^/user/([0-9]+)/post/([0-9]+)$"
|
||
replacement: "/posts/$1-$2"
|
||
flag: "redirect"
|
||
`
|
||
|
||
// 启动 lolly
|
||
lolly, err := testutil.StartLollyContainer(ctx, "", testutil.WithConfigYAML(config))
|
||
require.NoError(t, err, "Failed to start lolly container")
|
||
defer lolly.Terminate(ctx)
|
||
|
||
err = lolly.WaitForHealthy(ctx, 30*time.Second)
|
||
require.NoError(t, err, "Lolly not healthy")
|
||
|
||
client := &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||
return http.ErrUseLastResponse
|
||
}}
|
||
|
||
// 请求 /user/123/post/456 应该重定向到 /posts/123-456
|
||
resp, err := client.Get(lolly.HTTPBaseURL() + "/user/123/post/456")
|
||
require.NoError(t, err)
|
||
defer resp.Body.Close()
|
||
|
||
assert.Equal(t, http.StatusFound, resp.StatusCode)
|
||
|
||
location := resp.Header.Get("Location")
|
||
assert.Contains(t, location, "/posts/123-456", "Location should contain /posts/123-456")
|
||
t.Logf("Location: %s", location)
|
||
}
|
||
|
||
// TestE2ERewriteProxy 测试代理模式下的重写。
|
||
func TestE2ERewriteProxy(t *testing.T) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||
defer cancel()
|
||
|
||
if !testutil.LollyImageAvailable(ctx) {
|
||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||
}
|
||
|
||
// 启动模拟后端
|
||
backend, backendAddr, err := testutil.StartMockBackend(ctx)
|
||
require.NoError(t, err, "Failed to start mock backend")
|
||
defer backend.Terminate(ctx)
|
||
|
||
// 配置代理 + 重写
|
||
config := fmt.Sprintf(`
|
||
servers:
|
||
- listen: ":8080"
|
||
proxy:
|
||
- path: "/api"
|
||
targets:
|
||
- url: "http://%s"
|
||
rewrite:
|
||
- pattern: "^/api/v1/(.*)$"
|
||
replacement: "/api/v2/$1"
|
||
flag: "last"
|
||
`, backendAddr)
|
||
|
||
// 启动 lolly
|
||
lolly, err := testutil.StartLollyContainer(ctx, "", testutil.WithConfigYAML(config))
|
||
require.NoError(t, err, "Failed to start lolly container")
|
||
defer lolly.Terminate(ctx)
|
||
|
||
err = lolly.WaitForHealthy(ctx, 30*time.Second)
|
||
require.NoError(t, err, "Lolly not healthy")
|
||
|
||
client := &http.Client{Timeout: 10 * time.Second}
|
||
|
||
// 请求 /api/v1/users 应该被重写到 /api/v2/users
|
||
resp, err := client.Get(lolly.HTTPBaseURL() + "/api/v1/users")
|
||
require.NoError(t, err)
|
||
defer resp.Body.Close()
|
||
|
||
t.Logf("Status: %d", resp.StatusCode)
|
||
// 代理请求应该成功
|
||
assert.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||
}
|
||
|
||
// TestE2ERewriteNoMatch 测试不匹配时不重写。
|
||
func TestE2ERewriteNoMatch(t *testing.T) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
||
defer cancel()
|
||
|
||
if !testutil.LollyImageAvailable(ctx) {
|
||
t.Skip("lolly:latest image not available, run 'make docker-build' first")
|
||
}
|
||
|
||
// 配置重写规则
|
||
config := `
|
||
servers:
|
||
- listen: ":8080"
|
||
static:
|
||
- path: "/"
|
||
root: "/var/www/html"
|
||
rewrite:
|
||
- pattern: "^/old/(.*)$"
|
||
replacement: "/new/$1"
|
||
flag: "redirect"
|
||
`
|
||
|
||
// 启动 lolly
|
||
lolly, err := testutil.StartLollyContainer(ctx, "", testutil.WithConfigYAML(config))
|
||
require.NoError(t, err, "Failed to start lolly container")
|
||
defer lolly.Terminate(ctx)
|
||
|
||
err = lolly.WaitForHealthy(ctx, 30*time.Second)
|
||
require.NoError(t, err, "Lolly not healthy")
|
||
|
||
client := &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||
return http.ErrUseLastResponse
|
||
}}
|
||
|
||
// 请求 /other/test 不匹配规则,不应该重定向
|
||
resp, err := client.Get(lolly.HTTPBaseURL() + "/other/test")
|
||
require.NoError(t, err)
|
||
defer resp.Body.Close()
|
||
|
||
// 不应该返回重定向
|
||
assert.NotEqual(t, http.StatusFound, resp.StatusCode)
|
||
assert.NotEqual(t, http.StatusMovedPermanently, resp.StatusCode)
|
||
}
|