Files
mc-gateway/cmd/gateway/handle_request_test.go
tursom f508ecc1b9
Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
docs: 补充中文代码注释
2026-06-27 20:15:29 +08:00

386 lines
13 KiB
Go

// cmd/gateway/handle_request_test.go 包含用于约束 handle request 行为的测试。
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net"
"strings"
"testing"
"time"
"github.com/tursom/mc-gateway/internal/pluginmanager"
"github.com/tursom/mc-gateway/plugin/api"
"github.com/tursom/mc-gateway/protocol"
)
func TestHandleRequestProxiesAndClosesConnections(t *testing.T) {
defer saveGatewayState(t)()
packet := gatewayTestPacket("play.example")
source := newGatewayTestConn(packet)
upstream := newGatewayTestConn([]byte("reply"))
setGatewayTestRoutes(map[string]string{
"play.example": "backend.example:25565",
})
registerGatewayUpstreamHook(
t,
func(net.Conn, string) bool { return true },
func(net.Conn, string) (net.Conn, error) {
return upstream, nil
},
)
handleRequest(source)
if !source.closed {
t.Fatal("source connection was not closed")
}
if !upstream.closed {
t.Fatal("upstream connection was not closed")
}
if !bytes.Equal(upstream.writeBuf.Bytes(), packet) {
t.Fatalf("upstream initial packet = %v, want %v", upstream.writeBuf.Bytes(), packet)
}
if got := source.writeBuf.String(); got != "reply" {
t.Fatalf("proxied reply = %q, want reply", got)
}
}
func TestHandleRequestProtocolProxyReplaysInitialDataOnce(t *testing.T) {
defer saveGatewayState(t)()
packet := gatewayTestPacket("play.example", 0x63, 0x02)
source := newGatewayTestConn(packet)
setGatewayTestRoutes(map[string]string{
"play.example": "backend.example:25565",
})
pluginsManager = pluginmanager.New(pluginmanager.Options{
DB: newGatewayTestPluginDB(t),
ArtifactRoot: t.TempDir(),
Adapter: gatewayTestPluginAdapter{handler: func(req api.UpstreamConnectRequest) (net.Conn, error) {
if req.ServerHost != "play.example" || req.Host != "play.example" {
t.Fatalf("request host = %q/%q, want play.example", req.ServerHost, req.Host)
}
if req.ProtocolVersion != 0x63 || req.NextState != 0x02 {
t.Fatalf("protocol/next state = %d/%d, want 99/2", req.ProtocolVersion, req.NextState)
}
if !bytes.Equal(req.InitialData, packet) {
t.Fatalf("initial data = %v, want %v", req.InitialData, packet)
}
gatewayEnd, pluginEnd := net.Pipe()
go func() {
defer pluginEnd.Close()
buf := make([]byte, len(packet))
if _, err := io.ReadFull(pluginEnd, buf); err != nil {
t.Errorf("plugin endpoint ReadFull() error = %v", err)
return
}
if !bytes.Equal(buf, packet) {
t.Errorf("plugin endpoint initial data = %v, want %v", buf, packet)
return
}
if host := protocol.GetMcHost(buf); host != "play.example" {
t.Errorf("plugin endpoint host = %q, want play.example", host)
return
}
_, _ = pluginEnd.Write([]byte("login rejected"))
}()
return gatewayEnd, nil
}},
})
artifact := uploadGatewayTestArtifactWithCapabilities(t, pluginsManager, "proxy-plugin", gatewayProtocolProxyCapabilities())
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "proxy-plugin", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
approveGatewayPluginGovernanceForTest(t, "proxy-plugin", artifact.ID)
if _, err := pluginsManager.Enable(context.Background(), "admin", "proxy-plugin"); err != nil {
t.Fatalf("Enable() error = %v", err)
}
handleRequest(source)
if got := source.writeBuf.String(); got != "login rejected" {
t.Fatalf("source response = %q, want login rejected", got)
}
plan := pluginsManager.DispatchPlan(context.Background())
if len(plan.Handlers) != 1 {
t.Fatalf("dispatch handlers = %d, want 1", len(plan.Handlers))
}
if plan.Handlers[0].Mode != pluginmanager.UpstreamModeProtocolProxy {
t.Fatalf("handler mode = %q, want protocol-proxy", plan.Handlers[0].Mode)
}
if plan.Handlers[0].ProxyStarted != 1 || plan.Handlers[0].ProxyCompleted != 1 {
t.Fatalf("proxy lifecycle = started %d completed %d, want 1/1", plan.Handlers[0].ProxyStarted, plan.Handlers[0].ProxyCompleted)
}
}
func TestHandleRequestManagedErrBlockedClosesSource(t *testing.T) {
defer saveGatewayState(t)()
source := newGatewayTestConn(gatewayTestPacket("play.example"))
setGatewayTestRoutes(map[string]string{
"play.example": "backend.example:25565",
})
pluginsManager = pluginmanager.New(pluginmanager.Options{
DB: newGatewayTestPluginDB(t),
ArtifactRoot: t.TempDir(),
Adapter: gatewayTestPluginAdapter{handler: func(api.UpstreamConnectRequest) (net.Conn, error) {
return nil, api.ErrBlocked
}},
})
artifact := uploadGatewayTestArtifact(t, pluginsManager, "blocked-plugin")
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "blocked-plugin", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
if _, err := pluginsManager.Enable(context.Background(), "admin", "blocked-plugin"); err != nil {
t.Fatalf("Enable() error = %v", err)
}
handleRequest(source)
if !source.closed {
t.Fatal("source was not closed")
}
if source.writeBuf.Len() != 0 {
t.Fatalf("source response length = %d, want 0", source.writeBuf.Len())
}
}
func TestHandleRequestProtocolProxyPanicOnlyFailsCurrentConnection(t *testing.T) {
defer saveGatewayState(t)()
setGatewayTestRoutes(map[string]string{
"play.example": "backend.example:25565",
})
calls := 0
pluginsManager = pluginmanager.New(pluginmanager.Options{
DB: newGatewayTestPluginDB(t),
ArtifactRoot: t.TempDir(),
Adapter: gatewayTestPluginAdapter{handler: func(api.UpstreamConnectRequest) (net.Conn, error) {
calls++
if calls == 1 {
panic("boom")
}
upstream := newGatewayTestConn(nil)
upstream.writeBuf.WriteString("ok")
return upstream, nil
}},
})
artifact := uploadGatewayTestArtifact(t, pluginsManager, "panic-plugin")
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "panic-plugin", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
if _, err := pluginsManager.Enable(context.Background(), "admin", "panic-plugin"); err != nil {
t.Fatalf("Enable() error = %v", err)
}
first := newGatewayTestConn(gatewayTestPacket("play.example"))
handleRequest(first)
if !first.closed {
t.Fatal("first connection was not closed")
}
second := newGatewayTestConn(gatewayTestPacket("play.example"))
handleRequest(second)
if !second.closed {
t.Fatal("second connection was not closed")
}
plan := pluginsManager.DispatchPlan(context.Background())
if got := plan.Handlers[0].Panics; got != 1 {
t.Fatalf("panic count = %d, want 1", got)
}
if calls != 2 {
t.Fatalf("handler calls = %d, want 2", calls)
}
}
func TestHandleRequestProtocolProxyDisableSkipsNewConnections(t *testing.T) {
defer saveGatewayState(t)()
setGatewayTestRoutes(map[string]string{
"play.example": "backend.example:25565",
})
proxyCalls := 0
pluginsManager = pluginmanager.New(pluginmanager.Options{
DB: newGatewayTestPluginDB(t),
ArtifactRoot: t.TempDir(),
Adapter: gatewayTestPluginAdapter{handler: func(req api.UpstreamConnectRequest) (net.Conn, error) {
proxyCalls++
gatewayEnd, pluginEnd := net.Pipe()
initialLen := len(req.InitialData)
go func() {
defer pluginEnd.Close()
_, _ = io.ReadFull(pluginEnd, make([]byte, initialLen))
}()
return gatewayEnd, nil
}},
})
artifact := uploadGatewayTestArtifactWithCapabilities(t, pluginsManager, "proxy-plugin", gatewayProtocolProxyCapabilities())
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "proxy-plugin", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
approveGatewayPluginGovernanceForTest(t, "proxy-plugin", artifact.ID)
if _, err := pluginsManager.Enable(context.Background(), "admin", "proxy-plugin"); err != nil {
t.Fatalf("Enable() error = %v", err)
}
first := newGatewayTestConn(gatewayTestPacket("play.example"))
handleRequest(first)
if proxyCalls != 1 {
t.Fatalf("proxy calls after first request = %d, want 1", proxyCalls)
}
if _, err := pluginsManager.Disable(context.Background(), "admin", "proxy-plugin"); err != nil {
t.Fatalf("Disable() error = %v", err)
}
legacyUpstream := newGatewayTestConn(nil)
registerGatewayUpstreamHook(
t,
func(net.Conn, string) bool { return true },
func(net.Conn, string) (net.Conn, error) { return legacyUpstream, nil },
)
second := newGatewayTestConn(gatewayTestPacket("play.example"))
handleRequest(second)
if proxyCalls != 1 {
t.Fatalf("proxy calls after disable = %d, want still 1", proxyCalls)
}
if legacyUpstream.writeBuf.Len() == 0 {
t.Fatal("legacy upstream did not receive second request")
}
}
func TestHandleRequestRouteResolverUsesOverrideAndSQLiteFallback(t *testing.T) {
defer saveGatewayState(t)()
setGatewayTestRoutes(map[string]string{"fallback.example": "fallback-upstream:25565"})
var dialed []string
pluginsManager = pluginmanager.New(pluginmanager.Options{
DB: newGatewayTestPluginDB(t),
ArtifactRoot: t.TempDir(),
Adapter: gatewayTestPluginAdapter{initHook: func(gateway *pluginmanager.Gateway) error {
return api.RegisterHookHandler(gateway, api.HookRouteResolve,
func(api.RouteResolveRequest) bool { return true },
func(req api.RouteResolveRequest) (api.RouteDecision, error) {
if req.Host == "override.example" {
return api.RouteDecision{Action: api.RouteDecisionOverride, Upstream: "override-upstream:25565", CacheTTL: time.Minute}, nil
}
return api.RouteDecision{Action: api.RouteDecisionPass}, nil
})
}},
})
artifact := uploadGatewayTestArtifactWithManifest(t, pluginsManager, "route-plugin", func(manifest *pluginmanager.Manifest) {
manifest.ExtensionPoints = []pluginmanager.ExtensionPoint{{Type: "provider", Key: pluginmanager.ExtensionRouteResolve}}
manifest.Capabilities = json.RawMessage(`{"extension_points":["route.resolve/v1"]}`)
})
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "route-plugin", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
if _, err := pluginsManager.Enable(context.Background(), "admin", "route-plugin"); err != nil {
t.Fatalf("Enable() error = %v", err)
}
registerGatewayUpstreamHook(t, func(net.Conn, string) bool { return true }, func(_ net.Conn, host string) (net.Conn, error) {
dialed = append(dialed, host)
return newGatewayTestConn(nil), nil
})
handleRequest(newGatewayTestConn(gatewayTestPacket("override.example")))
handleRequest(newGatewayTestConn(gatewayTestPacket("fallback.example")))
if len(dialed) != 2 || dialed[0] != "override-upstream:25565" || dialed[1] != "fallback-upstream:25565" {
t.Fatalf("dialed = %+v, want override then sqlite fallback", dialed)
}
}
func TestHandleRequestStatusPingPluginRespondsPerHost(t *testing.T) {
defer saveGatewayState(t)()
pluginsManager = pluginmanager.New(pluginmanager.Options{
DB: newGatewayTestPluginDB(t),
ArtifactRoot: t.TempDir(),
Adapter: gatewayTestPluginAdapter{initHook: func(gateway *pluginmanager.Gateway) error {
return api.RegisterHookHandler(gateway, api.HookStatusPing,
func(api.StatusPingRequest) bool { return true },
func(req api.StatusPingRequest) (api.StatusPingResponse, error) {
return api.StatusPingResponse{MOTD: "hello " + req.Host, VersionText: "phase7", MaxPlayers: 100}, nil
})
}},
})
artifact := uploadGatewayTestArtifactWithManifest(t, pluginsManager, "status-plugin", func(manifest *pluginmanager.Manifest) {
manifest.ExtensionPoints = []pluginmanager.ExtensionPoint{{Type: "hook", Key: pluginmanager.ExtensionStatusPing}}
manifest.Capabilities = json.RawMessage(`{"extension_points":["status.ping/v1"]}`)
})
if _, err := pluginsManager.SetDesired(context.Background(), "admin", "status-plugin", artifact.ID, pluginmanager.DesiredEnabled, `{}`, 10); err != nil {
t.Fatalf("SetDesired() error = %v", err)
}
if _, err := pluginsManager.Enable(context.Background(), "admin", "status-plugin"); err != nil {
t.Fatalf("Enable() error = %v", err)
}
source := newGatewayTestConn(gatewayTestPacket("status.example", 0x63, 0x01))
handleRequest(source)
if got := source.writeBuf.String(); !strings.Contains(got, "hello status.example") {
t.Fatalf("status response = %q, want host MOTD", got)
}
}
func TestHandleRequestRecoversAndClosesConnection(t *testing.T) {
defer saveGatewayState(t)()
source := &panicReadGatewayConn{gatewayTestConn: newGatewayTestConn(nil)}
handleRequest(source)
if !source.closed {
t.Fatal("source connection was not closed after panic")
}
}
func TestGatewayHandleConnStartsRequestGoroutine(t *testing.T) {
defer saveGatewayState(t)()
source := newGatewayTestConn(nil)
source.readErr = errors.New("read failed")
(&Gateway{}).HandleConn(source)
deadline := time.After(2 * time.Second)
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
for {
select {
case <-deadline:
t.Fatal("timed out waiting for HandleConn goroutine")
case <-ticker.C:
if source.isClosed() {
return
}
}
}
}
func TestGatewayTestOpPanics(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("TestOp() did not panic")
}
}()
(&Gateway{}).TestOp()
}
type panicReadGatewayConn struct {
*gatewayTestConn
}
func (c *panicReadGatewayConn) Read([]byte) (int, error) {
panic("read panic")
}