gateway/app_test.go

226 lines
6.6 KiB
Go
Raw Permalink Normal View History

package main
import (
"context"
"encoding/json"
"apigo.cc/go/log"
"apigo.cc/go/redis"
"apigo.cc/go/service"
"io"
"net"
gohttp "net/http"
"os"
"strings"
"testing"
"time"
)
func TestGateway(t *testing.T) {
// 1. 设置服务发现和 Redis 依赖
registry := "127.0.0.1:6379"
rd := redis.GetRedis(registry, log.DefaultLogger)
if rd.Error != nil {
t.Skip("Redis is not available, skipping gateway integration tests")
return
}
// 清理测试数据
rd.Do("DEL", "gateway:host:gw.test", "gateway:hosts")
// 2. 启动一个后端测试服务 test-backend
service.Config.App = "test-backend"
service.Config.Listen = ":0"
service.Config.Register = registry
service.Host("*").GET("/hello", func(req *service.Request) string {
return "hello from backend, path: " + req.RequestURI
})
service.Host("*").GET("/hello/world", func(req *service.Request) string {
return "hello from backend, path: " + req.RequestURI
})
asBackend := service.AsyncStart()
defer asBackend.Stop()
time.Sleep(200 * time.Millisecond) // 等待后端启动和注册
// 3. 配置 Gateway
GatewayConf.Redis = registry
GatewayConf.Prefix = "gateway"
// 初始化网关的配置到 Redis
proxyRules := []service.ProxyRule{
{Path: "^/api/(.*)$", ToApp: "test-backend", ToPath: "/$1"},
{Path: "/direct", ToApp: "test-backend", ToPath: "/hello"},
{Path: "/v2/*", ToApp: "test-backend", ToPath: "/hello/*"},
}
rewriteRules := []service.RewriteRule{
{Path: "^/old-api/(.*)$", ToPath: "/api/$1"},
}
proxyJson, _ := json.Marshal(proxyRules)
rewriteJson, _ := json.Marshal(rewriteRules)
rd.Do("HSET", "gateway:host:gw.test", "proxies", string(proxyJson))
rd.Do("HSET", "gateway:host:gw.test", "rewrites", string(rewriteJson))
// 创建临时静态目录
tmpDir, _ := os.MkdirTemp("", "gateway-static")
defer os.RemoveAll(tmpDir)
_ = os.WriteFile(tmpDir+"/index.html", []byte("static content"), 0644)
staticConfig := map[string]string{
"/ui": tmpDir,
}
staticJson, _ := json.Marshal(staticConfig)
rd.Do("HSET", "gateway:host:gw.test", "statics", string(staticJson))
// 4. 启动 Gateway 应用
app := NewGatewayApp()
// 设置独立的端口给 Gateway 的 WebServer 避免与 backend 冲突
service.Config.App = "gateway"
service.Config.Listen = ":0"
// 重置发现,确保网关独立
service.GlobalDiscoverer = nil
// 配置网关可以通过 discover 找到 test-backend (网关也需要开启 discover)
service.Config.Register = registry
service.Config.Calls = map[string]service.CallConfig{
"test-backend": {Timeout: 1000},
}
ctx := context.Background()
err := app.Start(ctx, log.DefaultLogger)
if err != nil {
t.Fatalf("Failed to start gateway: %v", err)
}
defer app.Stop(ctx)
time.Sleep(200 * time.Millisecond)
_, gwPort, _ := net.SplitHostPort(app.Addr)
client := &gohttp.Client{Timeout: 2 * time.Second}
// ============================================
// 测试 1: 直接代理匹配 (Discover)
// ============================================
req, _ := gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/direct", nil)
req.Host = "gw.test"
res, err := client.Do(req)
var body string
if err == nil && res != nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
body = string(b)
}
if err != nil || body != "hello from backend, path: /hello" {
t.Fatalf("Proxy /direct failed, got: %s, err: %v", body, err)
}
// ============================================
// 测试 2: 正则代理匹配 (Discover)
// ============================================
req, _ = gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/api/hello?a=1", nil)
req.Host = "gw.test"
res, err = client.Do(req)
body = ""
if err == nil && res != nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
body = string(b)
}
if err != nil || !strings.Contains(body, "hello from backend, path: /hello?a=1") {
t.Fatalf("Proxy regexp failed, got: %s", body)
}
// ============================================
// 测试 2.1: 极速通配符前缀匹配 (Discover)
// ============================================
req, _ = gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/v2/world?x=2", nil)
req.Host = "gw.test"
res, err = client.Do(req)
body = ""
if err == nil && res != nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
body = string(b)
}
if err != nil || !strings.Contains(body, "hello from backend, path: /hello/world?x=2") {
t.Fatalf("Proxy wildcard failed, got: %s", body)
}
// ============================================
// 测试 3: Rewrite 后再 Proxy
// ============================================
req, _ = gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/old-api/hello", nil)
req.Host = "gw.test"
res, err = client.Do(req)
body = ""
if err == nil && res != nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
body = string(b)
}
if err != nil || body != "hello from backend, path: /hello" {
t.Fatalf("Rewrite+Proxy failed, got: %s", body)
}
// ============================================
// 测试 4: 静态文件服务
// ============================================
req, _ = gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/ui/index.html", nil)
req.Host = "gw.test"
res, err = client.Do(req)
body = ""
if err == nil && res != nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
body = string(b)
}
if err != nil || body != "static content" {
t.Fatalf("Static service failed, got: %s", body)
}
// ============================================
// 测试 5: 通过 Redis Pub/Sub 动态更新路由
// ============================================
// 新增一个新代理规则,删除老的
newProxyRules := []service.ProxyRule{
{Path: "/new-direct", ToApp: "test-backend", ToPath: "/hello"},
}
newProxyJson, _ := json.Marshal(newProxyRules)
rd.Do("HSET", "gateway:host:gw.test", "proxies", string(newProxyJson))
// 发布更新消息
rd.Do("PUBLISH", "gateway:channel", `"gw.test"`)
// 等待接收和更新
time.Sleep(300 * time.Millisecond)
// 测试新路由应该生效
req, _ = gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/new-direct", nil)
req.Host = "gw.test"
res, err = client.Do(req)
body = ""
if err == nil && res != nil {
b, _ := io.ReadAll(res.Body)
res.Body.Close()
body = string(b)
}
if err != nil || body != "hello from backend, path: /hello" {
t.Fatalf("Pub/Sub dynamic proxy failed, got: %s", body)
}
// 测试老路由应该 404 (原子替换)
req, _ = gohttp.NewRequest("GET", "http://127.0.0.1:"+gwPort+"/direct", nil)
req.Host = "gw.test"
res, err = client.Do(req)
if err != nil {
t.Fatalf("Request failed: %v", err)
}
if res.StatusCode != 404 {
t.Fatalf("Old proxy rule should be deleted (404), got status: %v", res.StatusCode)
}
res.Body.Close()
}