2026-05-12 23:16:06 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-13 00:18:48 +08:00
|
|
|
"context"
|
2026-05-12 23:16:06 +08:00
|
|
|
"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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清理测试数据
|
2026-05-12 23:53:22 +08:00
|
|
|
rd.Do("DEL", "gateway:host:gw.test", "gateway:hosts")
|
2026-05-12 23:16:06 +08:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
})
|
2026-05-13 00:49:29 +08:00
|
|
|
service.Host("*").GET("/hello/world", func(req *service.Request) string {
|
|
|
|
|
return "hello from backend, path: " + req.RequestURI
|
|
|
|
|
})
|
2026-05-12 23:16:06 +08:00
|
|
|
|
|
|
|
|
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"},
|
2026-05-13 00:49:29 +08:00
|
|
|
{Path: "/v2/*", ToApp: "test-backend", ToPath: "/hello/*"},
|
2026-05-12 23:16:06 +08:00
|
|
|
}
|
|
|
|
|
rewriteRules := []service.RewriteRule{
|
|
|
|
|
{Path: "^/old-api/(.*)$", ToPath: "/api/$1"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
proxyJson, _ := json.Marshal(proxyRules)
|
|
|
|
|
rewriteJson, _ := json.Marshal(rewriteRules)
|
|
|
|
|
|
2026-05-12 23:53:22 +08:00
|
|
|
rd.Do("HSET", "gateway:host:gw.test", "proxies", string(proxyJson))
|
|
|
|
|
rd.Do("HSET", "gateway:host:gw.test", "rewrites", string(rewriteJson))
|
2026-05-12 23:16:06 +08:00
|
|
|
|
|
|
|
|
// 创建临时静态目录
|
|
|
|
|
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)
|
2026-05-12 23:53:22 +08:00
|
|
|
rd.Do("HSET", "gateway:host:gw.test", "statics", string(staticJson))
|
2026-05-12 23:16:06 +08:00
|
|
|
|
|
|
|
|
// 4. 启动 Gateway 应用
|
|
|
|
|
app := NewGatewayApp()
|
2026-05-13 00:18:48 +08:00
|
|
|
|
|
|
|
|
// 设置独立的端口给 Gateway 的 WebServer 避免与 backend 冲突
|
2026-05-12 23:16:06 +08:00
|
|
|
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},
|
|
|
|
|
}
|
2026-05-13 00:18:48 +08:00
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
err := app.Start(ctx, log.DefaultLogger)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Failed to start gateway: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer app.Stop(ctx)
|
|
|
|
|
|
2026-05-12 23:16:06 +08:00
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
|
|
2026-05-13 00:18:48 +08:00
|
|
|
_, gwPort, _ := net.SplitHostPort(app.Addr)
|
2026-05-12 23:16:06 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 00:49:29 +08:00
|
|
|
// ============================================
|
|
|
|
|
// 测试 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 23:16:06 +08:00
|
|
|
// ============================================
|
|
|
|
|
// 测试 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)
|
2026-05-12 23:53:22 +08:00
|
|
|
rd.Do("HSET", "gateway:host:gw.test", "proxies", string(newProxyJson))
|
2026-05-12 23:16:06 +08:00
|
|
|
|
|
|
|
|
// 发布更新消息
|
2026-05-12 23:53:22 +08:00
|
|
|
rd.Do("PUBLISH", "gateway:channel", `"gw.test"`)
|
2026-05-12 23:16:06 +08:00
|
|
|
|
|
|
|
|
// 等待接收和更新
|
|
|
|
|
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()
|
|
|
|
|
}
|