diff --git a/handler/http/auth_test.go b/handler/http/auth_test.go index 565c746d..907fa74b 100644 --- a/handler/http/auth_test.go +++ b/handler/http/auth_test.go @@ -1,10 +1,17 @@ package http import ( + "bufio" "context" + "errors" + "io" "net" "net/http" + "net/url" + "os" + "strings" "testing" + "time" "github.com/go-gost/core/auth" "github.com/go-gost/core/handler" @@ -278,6 +285,290 @@ func TestCheckRateLimit_NilAddr(t *testing.T) { }() } +// --- probeResistanceResponse tests --- + +func TestProbeResistanceResponse_Web(t *testing.T) { + a := &Authenticator{ + Auther: &stubAuther{accept: false}, + PR: &probeResistance{Type: "web", Value: "example.com"}, + WebClient: func(url string) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{}, + Body: io.NopCloser(strings.NewReader("")), + }, nil + }, + } + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.Authenticate(context.Background(), req) + if result.OK { + t.Fatal("expected auth failure") + } + if result.Response == nil { + t.Fatal("expected non-nil response") + } + if result.Response.StatusCode != http.StatusOK { + t.Errorf("got status %d, want 200", result.Response.StatusCode) + } +} + +func TestProbeResistanceResponse_Web_Error(t *testing.T) { + a := &Authenticator{ + Auther: &stubAuther{accept: false}, + PR: &probeResistance{Type: "web", Value: "example.com"}, + Log: &testLogger{}, + WebClient: func(url string) (*http.Response, error) { + return nil, errors.New("network error") + }, + } + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.Authenticate(context.Background(), req) + if result.OK { + t.Fatal("expected auth failure") + } + // Web error → break → stays at 503 (the initial default) + if result.Response.StatusCode != http.StatusServiceUnavailable { + t.Errorf("got status %d, want 503 (default after web error)", result.Response.StatusCode) + } +} + +func TestProbeResistanceResponse_Web_NoPrefix(t *testing.T) { + // Value without http:// prefix gets prefixed automatically + var calledURL string + a := &Authenticator{ + Auther: &stubAuther{accept: false}, + PR: &probeResistance{Type: "web", Value: "example.com/path"}, + WebClient: func(url string) (*http.Response, error) { + calledURL = url + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{}, + Body: io.NopCloser(strings.NewReader("ok")), + }, nil + }, + } + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.Authenticate(context.Background(), req) + if result.OK { + t.Fatal("expected auth failure") + } + if !strings.HasPrefix(calledURL, "http://") { + t.Errorf("expected http:// prefix, got %q", calledURL) + } +} + +func TestProbeResistanceResponse_File(t *testing.T) { + f, err := os.CreateTemp("", "gost-pr-test-*.html") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + f.WriteString("decoy") + f.Close() + + a := &Authenticator{ + Auther: &stubAuther{accept: false}, + PR: &probeResistance{Type: "file", Value: f.Name()}, + } + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.Authenticate(context.Background(), req) + if result.OK { + t.Fatal("expected auth failure") + } + if result.Response == nil { + t.Fatal("expected non-nil response") + } + if result.Response.StatusCode != http.StatusOK { + t.Errorf("got status %d, want 200", result.Response.StatusCode) + } + if result.Response.Body == nil { + t.Error("expected body for file probe resistance") + } else { + result.Response.Body.Close() + } + if result.Response.Header.Get("Content-Type") != "text/html" { + t.Error("expected Content-Type: text/html") + } +} + +func TestProbeResistanceResponse_File_Missing(t *testing.T) { + a := &Authenticator{ + Auther: &stubAuther{accept: false}, + Realm: "gost", + PR: &probeResistance{Type: "file", Value: "/nonexistent/file.html"}, + } + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.Authenticate(context.Background(), req) + if result.OK { + t.Fatal("expected auth failure") + } + // Falls back to default 503 when file not found + if result.Response.StatusCode != http.StatusServiceUnavailable { + t.Errorf("got status %d, want 503 (fallback when file missing)", result.Response.StatusCode) + } +} + +func TestProbeResistanceResponse_Web_StatusOK_KeepAlive(t *testing.T) { + a := &Authenticator{ + Auther: &stubAuther{accept: false}, + PR: &probeResistance{Type: "web", Value: "example.com"}, + WebClient: func(url string) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{}, + Body: io.NopCloser(strings.NewReader("ok")), + }, nil + }, + } + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.Authenticate(context.Background(), req) + if result.OK { + t.Fatal("expected auth failure") + } + if result.Response.Header.Get("Connection") != "keep-alive" { + t.Errorf("expected Connection: keep-alive for 200 response, got %q", + result.Response.Header.Get("Connection")) + } +} + +func TestProbeResistanceResponse_StatusCodeZero(t *testing.T) { + // When PR type is unknown, resp.StatusCode stays 0 which triggers build407Response fallback + a := &Authenticator{ + Auther: &stubAuther{accept: false}, + Realm: "gost", + PR: &probeResistance{Type: "unknown", Value: "x"}, + } + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.Authenticate(context.Background(), req) + if result.OK { + t.Fatal("expected auth failure") + } + if result.Response.StatusCode != http.StatusServiceUnavailable { + t.Errorf("got status %d, want 503 (default for unknown type)", result.Response.StatusCode) + } +} + +func TestProbeResistanceResponse_Code_Invalid_Logs(t *testing.T) { + a := &Authenticator{ + Auther: &stubAuther{accept: false}, + Realm: "gost", + PR: &probeResistance{Type: "code", Value: "not-a-number"}, + Log: &testLogger{}, + } + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.Authenticate(context.Background(), req) + if result.OK { + t.Fatal("expected auth failure") + } + // Falls back to 503 (default), then 407 because probeResistanceResponse calls build407 for 503 + if result.Response.StatusCode != http.StatusServiceUnavailable { + t.Errorf("got status %d, want 503 (invalid code keeps default)", result.Response.StatusCode) + } +} + +// --- build407Response tests --- + +func TestBuild407Response_CustomRealm(t *testing.T) { + a := &Authenticator{Realm: "custom"} + req, _ := http.NewRequest("GET", "http://example.com/", nil) + result := a.build407Response(req) + if result.Response.Header.Get("Proxy-Authenticate") == "" { + t.Error("expected Proxy-Authenticate header") + } + if !strings.Contains(result.Response.Header.Get("Proxy-Authenticate"), "custom") { + t.Error("expected custom realm in Proxy-Authenticate") + } +} + +func TestBuild407Response_ProxyConnectionKeepAlive(t *testing.T) { + a := &Authenticator{} + req, _ := http.NewRequest("GET", "http://example.com/", nil) + req.Header.Set("Proxy-Connection", "keep-alive") + result := a.build407Response(req) + if result.Response.Header.Get("Connection") != "close" { + t.Errorf("got Connection=%q, want close", result.Response.Header.Get("Connection")) + } + if result.Response.Header.Get("Proxy-Connection") != "close" { + t.Errorf("got Proxy-Connection=%q, want close", result.Response.Header.Get("Proxy-Connection")) + } +} + +// --- handleProbeResistanceHost tests --- + +func TestHandleProbeResistanceHost_DialFailure(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.md.proxyAgent = defaultProxyAgent + + conn := newStringConn("") + resp := &http.Response{ + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header{}, + ContentLength: -1, + } + err := h.handleProbeResistanceHost(context.Background(), conn, nil, "127.0.0.1:99999", &testLogger{}, resp) + if err != nil { + t.Logf("handleProbeResistanceHost error (expected): %v", err) + } + // Should have written a 503 to conn + written := conn.String() + if !strings.Contains(written, "503") { + t.Errorf("expected 503 in response, got: %s", written) + } +} + +func TestHandleProbeResistanceHost_Success(t *testing.T) { + // Start a listener to accept the probe resistance host connection + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer l.Close() + + ready := make(chan struct{}) + go func() { + cc, _ := l.Accept() + ready <- struct{}{} + // Read the request so the pipe doesn't block + br := bufio.NewReader(cc) + http.ReadRequest(br) + cc.Close() + }() + + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + + conn := newStringConn("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + resp := &http.Response{ + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header{}, + ContentLength: -1, + } + + // handleProbeResistanceHost dials the target and pipes the request + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + err = h.handleProbeResistanceHost(ctx, conn, &http.Request{ + Method: "GET", + URL: &url.URL{Host: "example.com"}, + Header: http.Header{}, + Body: io.NopCloser(strings.NewReader("")), + Proto: "HTTP/1.1", + }, l.Addr().String(), &testLogger{}, resp) + <-ready + // Error from the pipe is expected since the accepted conn will be closed + t.Logf("handleProbeResistanceHost result: %v", err) +} + // --- Stub implementations (used by multiple test files) --- // stubRateLimiter implements rate.RateLimiter for testing. diff --git a/handler/http/connect_test.go b/handler/http/connect_test.go index bef4139c..4cce4a88 100644 --- a/handler/http/connect_test.go +++ b/handler/http/connect_test.go @@ -6,8 +6,10 @@ import ( "net" "net/http" "testing" + "time" "github.com/go-gost/core/handler" + stats_util "github.com/go-gost/x/internal/util/stats" xrecorder "github.com/go-gost/x/recorder" ) @@ -157,3 +159,77 @@ func TestDial_HostHash(t *testing.T) { t.Error("expected error from dial with nil router") } } + +func TestSetupTrafficLimiter_WithObserver(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + Service: "test", + Observer: &testObserver{}, + }, + } + h.stats = stats_util.NewHandlerStats("test", false) + + client, _ := net.Pipe() + defer client.Close() + + result, done := h.setupTrafficLimiter(client, "test-client", "tcp", "example.com:80") + if result == nil { + t.Error("expected non-nil connection") + } + if done == nil { + t.Error("expected non-nil cleanup function when observer is set") + } + done() +} + +func TestSniffAndHandle_Timeout(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.md.sniffing = true + h.md.sniffingTimeout = 100 * time.Millisecond + h.sniffer = &SnifferBuilder{} + + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + go func() { + // Write non-HTTP, non-TLS bytes slowly + client.Write([]byte("NOT_HTTP_OR_TLS_DATA")) + }() + + cc, _ := net.Pipe() + defer cc.Close() + + handled, err := h.sniffAndHandle(context.Background(), server, cc, &xrecorder.HandlerRecorderObject{}, &testLogger{}) + if err != nil { + t.Logf("sniff error: %v", err) + } + if handled { + t.Error("expected not handled for unknown protocol") + } +} + +func TestHandleConnect_WriteError(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.md.proxyAgent = defaultProxyAgent + h.md.readTimeout = 15 + + client, server := net.Pipe() + // Immediately close client so write fails + client.Close() + server.Close() + + resp := testConnectResp() + err := h.handleConnect(context.Background(), server, &xrecorder.HandlerRecorderObject{}, &testLogger{}, "example.com:443", resp) + // Error from reading the closed conn + t.Logf("handleConnect with closed conn: %v", err) +} diff --git a/handler/http/handler_test.go b/handler/http/handler_test.go index 83892aa4..c80df54c 100644 --- a/handler/http/handler_test.go +++ b/handler/http/handler_test.go @@ -4,10 +4,13 @@ import ( "context" "io" "net" + "net/http" "testing" "time" "github.com/go-gost/core/handler" + stats_util "github.com/go-gost/x/internal/util/stats" + xrecorder "github.com/go-gost/x/recorder" ) @@ -206,3 +209,141 @@ func TestSetupTrafficLimiter_NoObserver(t *testing.T) { } } +func TestHandleRequest_Bypass(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + Bypass: &testBypass{contains: true}, + }, + } + h.md.proxyAgent = defaultProxyAgent + h.md.readTimeout = 15 + h.auth = &Authenticator{} + + conn := newStringConn("") + req, _ := http.NewRequest("GET", "http://example.com:80/path", nil) + + err := h.handleRequest(context.Background(), conn, req, &xrecorder.HandlerRecorderObject{}, &testLogger{}) + if err == nil { + t.Error("expected bypass error") + } +} + +func TestHandleRequest_UDP_Disabled(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.md.proxyAgent = defaultProxyAgent + h.md.readTimeout = 15 + h.auth = &Authenticator{} + + conn := newStringConn("") + req, _ := http.NewRequest("GET", "http://example.com:80/path", nil) + req.Header.Set("X-Gost-Protocol", "udp") + + err := h.handleRequest(context.Background(), conn, req, &xrecorder.HandlerRecorderObject{}, &testLogger{}) + // UDP disabled → 403 Forbidden written + if err != nil { + t.Logf("handleRequest UDP disabled: %v", err) + } +} + +func TestHandleRequest_CONNECT_NonConnectMethod_NoScheme(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.md.proxyAgent = defaultProxyAgent + h.md.readTimeout = 15 + h.auth = &Authenticator{} + + conn := newStringConn("") + req, _ := http.NewRequest("PRI", "*", nil) + + err := h.handleRequest(context.Background(), conn, req, &xrecorder.HandlerRecorderObject{}, &testLogger{}) + if err != nil { + t.Logf("handleRequest PRI: %v", err) + } +} + +func TestHandleRequest_ProbeResistanceAuth(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.md.proxyAgent = defaultProxyAgent + h.md.readTimeout = 15 + h.auth = &Authenticator{ + Auther: &stubAuther{accept: false}, + PR: &probeResistance{Type: "code", Value: "404"}, + } + + conn := newStringConn("") + req, _ := http.NewRequest("GET", "http://example.com:80/path", nil) + + err := h.handleRequest(context.Background(), conn, req, &xrecorder.HandlerRecorderObject{}, &testLogger{}) + if err == nil { + t.Error("expected auth failure") + } +} + +func TestObserveStats_NilObserver(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.observeStats(context.Background()) +} + +func TestObserveStats_SendsEvents(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + Service: "test", + Observer: &testObserver{}, + }, + } + h.md.observerPeriod = 50 * time.Millisecond + h.stats = stats_util.NewHandlerStats("test", false) + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + done := make(chan struct{}) + go func() { + h.observeStats(ctx) + close(done) + }() + + select { + case <-done: + case <-time.After(time.Second): + t.Error("observeStats did not exit after context cancellation") + } +} + +func TestHandleRequest_Bypass_Test(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + Bypass: &testBypass{contains: true}, + }, + } + h.md.proxyAgent = defaultProxyAgent + h.md.readTimeout = 15 + h.auth = &Authenticator{} + + conn := newStringConn("") + req, _ := http.NewRequest("GET", "http://example.com:80/path", nil) + + err := h.handleRequest(context.Background(), conn, req, &xrecorder.HandlerRecorderObject{}, &testLogger{}) + if err == nil { + t.Error("expected bypass error") + } +} + diff --git a/handler/http/helpers_test.go b/handler/http/helpers_test.go index 0cc70023..283dfffb 100644 --- a/handler/http/helpers_test.go +++ b/handler/http/helpers_test.go @@ -5,6 +5,7 @@ import ( "net" "strings" "sync" + "time" "github.com/go-gost/core/handler" "github.com/go-gost/core/logger" @@ -100,8 +101,8 @@ func (c *stringConn) Close() error { } func (c *stringConn) LocalAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080} } func (c *stringConn) RemoteAddr() net.Addr { return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 12345} } -func (c *stringConn) SetDeadline(t any) error { return nil } -func (c *stringConn) SetReadDeadline(t any) error { return nil } -func (c *stringConn) SetWriteDeadline(t any) error { return nil } +func (c *stringConn) SetDeadline(t time.Time) error { return nil } +func (c *stringConn) SetReadDeadline(t time.Time) error { return nil } +func (c *stringConn) SetWriteDeadline(t time.Time) error { return nil } func (c *stringConn) Bytes() []byte { return []byte(c.writeBuf.String()) } func (c *stringConn) String() string { return c.writeBuf.String() } diff --git a/handler/http/metadata_test.go b/handler/http/metadata_test.go index fcf7b508..73406430 100644 --- a/handler/http/metadata_test.go +++ b/handler/http/metadata_test.go @@ -249,3 +249,81 @@ func TestParseMetadata_LimiterIntervals(t *testing.T) { t.Errorf("got cleanupInterval %v, want 60s", h.md.limiterCleanupInterval) } } + +func TestParseMetadata_ObserverResetTraffic(t *testing.T) { + h := &httpHandler{} + if err := h.parseMetadata(testMD(map[string]any{"observer.resetTraffic": "true"})); err != nil { + t.Fatal(err) + } + if !h.md.observerResetTraffic { + t.Error("expected observer.resetTraffic to be true") + } +} + +func TestParseMetadata_MITM_Config(t *testing.T) { + h := &httpHandler{} + if err := h.parseMetadata(testMD(map[string]any{ + "mitm.certFile": "/nonexistent.pem", + "mitm.keyFile": "/nonexistent.key", + })); err == nil { + t.Error("expected error for nonexistent cert/key files") + } +} + +func TestParseMetadata_MITM_ALPN(t *testing.T) { + h := &httpHandler{} + if err := h.parseMetadata(testMD(map[string]any{ + "mitm.alpn": "h2", + })); err != nil { + t.Fatal(err) + } + if h.md.alpn != "h2" { + t.Errorf("got alpn %q, want h2", h.md.alpn) + } +} + +func TestParseMetadata_MITM_Bypass(t *testing.T) { + h := &httpHandler{} + if err := h.parseMetadata(testMD(map[string]any{ + "mitm.bypass": "", + })); err != nil { + t.Fatal(err) + } + if h.md.mitmBypass != nil { + t.Error("expected nil mitmBypass for empty bypass name") + } +} + +func TestParseMetadata_ObserverPeriod_Zero(t *testing.T) { + h := &httpHandler{} + if err := h.parseMetadata(testMD(map[string]any{"observePeriod": "0"})); err != nil { + t.Fatal(err) + } + if h.md.observerPeriod != 5*time.Second { + t.Errorf("got observerPeriod %v, want 5s (default for zero)", h.md.observerPeriod) + } +} + +func TestParseMetadata_Headers_AltKey(t *testing.T) { + h := &httpHandler{} + if err := h.parseMetadata(testMD(map[string]any{ + "header": map[string]any{ + "X-Custom": "value-from-alt-key", + }, + })); err != nil { + t.Fatal(err) + } + if h.md.header.Get("X-Custom") != "value-from-alt-key" { + t.Errorf("got header X-Custom=%q, want value-from-alt-key", h.md.header.Get("X-Custom")) + } +} + +func TestParseMetadata_ProxyAgent_AltKey(t *testing.T) { + h := &httpHandler{} + if err := h.parseMetadata(testMD(map[string]any{"http.proxyAgent": "gost-alt/1.0"})); err != nil { + t.Fatal(err) + } + if h.md.proxyAgent != "gost-alt/1.0" { + t.Errorf("got proxyAgent %q, want gost-alt/1.0", h.md.proxyAgent) + } +} diff --git a/handler/http/proxy_test.go b/handler/http/proxy_test.go index fc618289..3b6da4b1 100644 --- a/handler/http/proxy_test.go +++ b/handler/http/proxy_test.go @@ -162,6 +162,7 @@ func TestProxyRoundTrip_HTTP10(t *testing.T) { } } + func TestHandleUpgradeResponse_Mismatch(t *testing.T) { h := &httpHandler{} h.md.proxyAgent = defaultProxyAgent @@ -345,6 +346,39 @@ func (e *testError) Error() string { return e.msg } func (e *testError) Timeout() bool { return false } func (e *testError) Temporary() bool { return true } +func TestProxyRoundTrip_HTTP10_KeepAlive(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // For HTTP/1.0 keep-alive, client sends Connection: keep-alive + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + transport: ts.Client().Transport, + } + h.md.readTimeout = 15 + + req, _ := http.NewRequest("GET", ts.URL, nil) + req.ProtoMajor = 1 + req.ProtoMinor = 0 + req.Header.Set("Connection", "keep-alive") // client wants keep-alive + + ro := &xrecorder.HandlerRecorderObject{ + RemoteAddr: "127.0.0.1:12345", + } + pStats := xstats.Stats{} + rw := &testReadWriteCloser{buf: new(strings.Builder)} + + _, err := h.proxyRoundTrip(context.Background(), rw, req, ro, &pStats, &testLogger{}) + if err != nil { + t.Fatalf("proxyRoundTrip error: %v", err) + } +} + + // testReadWriteCloser implements io.ReadWriteCloser for testing. type testReadWriteCloser struct { buf *strings.Builder diff --git a/handler/http/websocket_test.go b/handler/http/websocket_test.go index d7e1d91c..4fbfa99f 100644 --- a/handler/http/websocket_test.go +++ b/handler/http/websocket_test.go @@ -8,6 +8,7 @@ import ( "github.com/go-gost/core/handler" "github.com/go-gost/core/recorder" + xrecorder "github.com/go-gost/x/recorder" ) func TestCopyWebsocketFrame_ReadError(t *testing.T) { @@ -42,6 +43,79 @@ func TestCopyWebsocketDirection_ErrorReader(t *testing.T) { } } +func TestCopyWebsocketFrame_WithBodyRecording(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.recorder = recorder.RecorderObject{ + Options: &recorder.Options{ + HTTPBody: true, + MaxBodySize: 1024, + }, + } + + // Build a minimal WebSocket text frame (fin=1, opcode=1, payload="hello") + frame := []byte{0x81, 0x05, 'h', 'e', 'l', 'l', 'o'} + buf := &bytes.Buffer{} + + ro := &xrecorder.HandlerRecorderObject{} + err := h.copyWebsocketFrame(io.Discard, bytes.NewReader(frame), buf, "client", ro) + if err != nil { + t.Fatalf("copyWebsocketFrame: %v", err) + } + if ro.Websocket == nil { + t.Fatal("expected Websocket recorder object") + } + if ro.Websocket.OpCode != 1 { // text frame + t.Errorf("got OpCode %d, want 1", ro.Websocket.OpCode) + } + if ro.InputBytes == 0 { + t.Error("expected non-zero InputBytes for client frame") + } + if ro.OutputBytes != 0 { + t.Error("expected zero OutputBytes for client frame") + } +} + +func TestCopyWebsocketFrame_ServerDirection(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.recorder = recorder.RecorderObject{} + + frame := []byte{0x82, 0x03, 'a', 'b', 'c'} // binary frame + buf := &bytes.Buffer{} + + ro := &xrecorder.HandlerRecorderObject{} + err := h.copyWebsocketFrame(io.Discard, bytes.NewReader(frame), buf, "server", ro) + if err != nil { + t.Fatalf("copyWebsocketFrame: %v", err) + } + if ro.InputBytes != 0 { + t.Error("expected zero InputBytes for server frame") + } + if ro.OutputBytes == 0 { + t.Error("expected non-zero OutputBytes for server frame") + } +} + +func TestCopyWebsocketDirection_NilRO(t *testing.T) { + h := &httpHandler{ + options: handler.Options{ + Logger: &testLogger{}, + }, + } + h.recorder = recorder.RecorderObject{} + + errc := make(chan error, 1) + go h.copyWebsocketDirection(context.Background(), &errorReader{}, io.Discard, "client", nil, 10.0, &testLogger{}, errc) + <-errc +} + // errorReader always returns an error. type errorReader struct{}