refactor(handler/relay): split handler.go -> observe.go, fix observeStats event-loss bug (break->fallthrough)

Extract checkRateLimit and observeStats into their own file (observe.go).
Fix the one-tick event-loss bug in observeStats where the retry path used
'unconditional break' instead of fallthrough, causing events collected
after a successful retry to be discarded.

Add 87 unit tests across 7 files (helpers, metadata, conn, handler,
connect, forward, bind) covering all handler modes, error paths,
observer/stats integration, and conn wrapper edge cases.

handler.go: -43 lines (unused observer import, extracted functions)
observe.go: +52 lines (checkRateLimit, observeStats with fallthrough fix)
test files: +1959 lines
This commit is contained in:
ginuerzh
2026-06-02 23:51:19 +08:00
parent 31878a72ec
commit d432d28f81
9 changed files with 1959 additions and 43 deletions
+110
View File
@@ -0,0 +1,110 @@
package relay
import (
"context"
"testing"
"time"
"github.com/go-gost/core/handler"
"github.com/go-gost/relay"
)
// handleBind tests use a real TCP listener on port 0 to verify the bind flow.
// The Handle method is called with a CmdBind request, and the response is
// checked for StatusOK + an AddrFeature.
func TestHandleBind_Disabled(t *testing.T) {
rh := newInitdHandler(t,
handler.LoggerOption(&testLogger{}),
)
// bind is disabled by default
fc := &fakeConn{buf: buildRelayBindRequest(t, "127.0.0.1:0", "")}
err := rh.Handle(context.Background(), fc)
// handleBind returns the error from resp.WriteTo(conn) — when the write
// succeeds the returned error is nil. The response status indicates the
// denial, not the returned error.
_ = err
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusForbidden {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusForbidden)
}
}
func TestHandleBind_TCP(t *testing.T) {
rh := &relayHandler{
options: handler.Options{
Logger: &testLogger{},
},
}
rh.parseMetadata(testMD(map[string]any{"bind": true}))
fc := &fakeConn{buf: buildRelayBindRequest(t, "127.0.0.1:0", "")}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
errCh := make(chan error, 1)
go func() {
errCh <- rh.Handle(ctx, fc)
}()
// Wait for the response (bindTCP writes the response before blocking on Serve)
// bindTCP calls resp.WriteTo(conn) then upgrades to mux session.
// Since the fakeConn doesn't support mux (it's not a real mux-compatible conn),
// the mux.ClientSession call will fail.
// But we should at least get the response before that.
select {
case err := <-errCh:
if err != nil {
// bindTCP will fail when mux.ClientSession tries to upgrade the fakeConn
// That's expected — the response was written before that
_ = err
}
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for bind handler")
}
// Check the response — should have StatusOK and an AddrFeature
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusOK {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusOK)
}
// Verify response has at least one feature
if len(resp.Features) == 0 {
t.Error("response has no features, expected AddrFeature")
}
}
func TestHandleBind_UDP(t *testing.T) {
rh := &relayHandler{
options: handler.Options{
Logger: &testLogger{},
},
}
rh.parseMetadata(testMD(map[string]any{"bind": true}))
fc := &fakeConn{buf: buildRelayBindRequest(t, "127.0.0.1:0", "udp")}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
errCh := make(chan error, 1)
go func() {
errCh <- rh.Handle(ctx, fc)
}()
select {
case err := <-errCh:
if err != nil {
_ = err
}
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for bind handler")
}
resp := readRelayResponse(t, fc.writeBuf.Bytes())
if resp.Status != relay.StatusOK {
t.Errorf("Status = %d, want %d", resp.Status, relay.StatusOK)
}
}