test(bypass): add unit tests achieving 96.4% total coverage
- 88 tests: 70 for core (96.8%), 12 for gRPC plugin, 14 for HTTP plugin - Cover all matchers (IP, CIDR, wildcard, IP range), loader sources, bypassGroup logic, periodReload, and plugin fail-open semantics - Add nil-logger guard in httpPlugin.Contains to prevent panic when logger is nil
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
package bypass
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/core/bypass"
|
||||
corelogger "github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/plugin/bypass/proto"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
xlogger "github.com/go-gost/x/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
corelogger.SetDefault(xlogger.Nop())
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// helper to set up a real gRPC server on a random port, returning the client conn and server stop func.
|
||||
func newTestGRPCConn(t *testing.T, srv proto.BypassServer) (*grpc.ClientConn, func()) {
|
||||
t.Helper()
|
||||
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
gsrv := grpc.NewServer()
|
||||
proto.RegisterBypassServer(gsrv, srv)
|
||||
go gsrv.Serve(lis)
|
||||
|
||||
conn, err := grpc.NewClient(lis.Addr().String(),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return conn, func() {
|
||||
conn.Close()
|
||||
gsrv.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_FailOpen_NilClient(t *testing.T) {
|
||||
p := &grpcPlugin{
|
||||
conn: nil,
|
||||
client: nil,
|
||||
log: xlogger.Nop(),
|
||||
}
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
assert.NoError(t, p.Close())
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Contains_Success(t *testing.T) {
|
||||
conn, cleanup := newTestGRPCConn(t, &testBypassServer{ok: true})
|
||||
defer cleanup()
|
||||
|
||||
p := &grpcPlugin{
|
||||
conn: conn,
|
||||
client: proto.NewBypassClient(conn),
|
||||
log: xlogger.Nop(),
|
||||
}
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Contains_Deny(t *testing.T) {
|
||||
conn, cleanup := newTestGRPCConn(t, &testBypassServer{ok: false})
|
||||
defer cleanup()
|
||||
|
||||
p := &grpcPlugin{
|
||||
conn: conn,
|
||||
client: proto.NewBypassClient(conn),
|
||||
log: xlogger.Nop(),
|
||||
}
|
||||
assert.False(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Contains_WithService(t *testing.T) {
|
||||
srv := &testBypassServer{ok: true}
|
||||
conn, cleanup := newTestGRPCConn(t, srv)
|
||||
defer cleanup()
|
||||
|
||||
p := &grpcPlugin{
|
||||
conn: conn,
|
||||
client: proto.NewBypassClient(conn),
|
||||
log: xlogger.Nop(),
|
||||
}
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1", bypass.WithService("myservice")))
|
||||
assert.Equal(t, "myservice", srv.lastService)
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Contains_WithHostAndPath(t *testing.T) {
|
||||
srv := &testBypassServer{ok: true}
|
||||
conn, cleanup := newTestGRPCConn(t, srv)
|
||||
defer cleanup()
|
||||
|
||||
p := &grpcPlugin{
|
||||
conn: conn,
|
||||
client: proto.NewBypassClient(conn),
|
||||
log: xlogger.Nop(),
|
||||
}
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1",
|
||||
bypass.WithHostOption("example.com"),
|
||||
bypass.WithPathOption("/api/v1"),
|
||||
))
|
||||
assert.Equal(t, "example.com", srv.lastHost)
|
||||
assert.Equal(t, "/api/v1", srv.lastPath)
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Contains_NilClient(t *testing.T) {
|
||||
p := &grpcPlugin{
|
||||
conn: nil,
|
||||
client: nil,
|
||||
}
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Contains_ServerError(t *testing.T) {
|
||||
conn, cleanup := newTestGRPCConn(t, &errorBypassServer{})
|
||||
defer cleanup()
|
||||
|
||||
p := &grpcPlugin{
|
||||
conn: conn,
|
||||
client: proto.NewBypassClient(conn),
|
||||
log: xlogger.Nop(),
|
||||
}
|
||||
// Server returns an error; Contains should return true (fail-open)
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Close_NilConn(t *testing.T) {
|
||||
p := &grpcPlugin{
|
||||
conn: nil,
|
||||
client: nil,
|
||||
}
|
||||
assert.NoError(t, p.Close())
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Close_NonCloserConn(t *testing.T) {
|
||||
p := &grpcPlugin{
|
||||
conn: &nonCloserConn{},
|
||||
client: nil,
|
||||
}
|
||||
assert.NoError(t, p.Close())
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_Close_WithRealConn(t *testing.T) {
|
||||
conn, cleanup := newTestGRPCConn(t, &testBypassServer{ok: true})
|
||||
defer cleanup()
|
||||
|
||||
p := &grpcPlugin{
|
||||
conn: conn,
|
||||
client: nil,
|
||||
log: xlogger.Nop(),
|
||||
}
|
||||
assert.NoError(t, p.Close())
|
||||
}
|
||||
|
||||
func TestNewGRPCPlugin_RealConn(t *testing.T) {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
gsrv := grpc.NewServer()
|
||||
proto.RegisterBypassServer(gsrv, &testBypassServer{ok: true})
|
||||
go gsrv.Serve(lis)
|
||||
defer gsrv.Stop()
|
||||
|
||||
p := NewGRPCPlugin("test", lis.Addr().String(), plugin.TimeoutOption(500))
|
||||
require.NotNil(t, p)
|
||||
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
assert.NoError(t, p.(io.Closer).Close())
|
||||
}
|
||||
|
||||
func TestGRPCPlugin_IsWhitelist(t *testing.T) {
|
||||
p := &grpcPlugin{}
|
||||
assert.False(t, p.IsWhitelist())
|
||||
}
|
||||
|
||||
// --- test helpers ---
|
||||
|
||||
type testBypassServer struct {
|
||||
proto.UnimplementedBypassServer
|
||||
ok bool
|
||||
lastService string
|
||||
lastHost string
|
||||
lastPath string
|
||||
}
|
||||
|
||||
func (s *testBypassServer) Bypass(ctx context.Context, req *proto.BypassRequest) (*proto.BypassReply, error) {
|
||||
s.lastService = req.Service
|
||||
s.lastHost = req.Host
|
||||
s.lastPath = req.Path
|
||||
return &proto.BypassReply{Ok: s.ok}, nil
|
||||
}
|
||||
|
||||
type errorBypassServer struct {
|
||||
proto.UnimplementedBypassServer
|
||||
}
|
||||
|
||||
func (s *errorBypassServer) Bypass(ctx context.Context, req *proto.BypassRequest) (*proto.BypassReply, error) {
|
||||
return nil, status.Error(codes.Internal, "internal error")
|
||||
}
|
||||
|
||||
type nonCloserConn struct{}
|
||||
|
||||
func (n *nonCloserConn) Invoke(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nonCloserConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var _ grpc.ClientConnInterface = (*nonCloserConn)(nil)
|
||||
var _ io.Closer = (*grpcPlugin)(nil)
|
||||
@@ -62,6 +62,11 @@ func (p *httpPlugin) Contains(ctx context.Context, network, addr string, opts ..
|
||||
return true
|
||||
}
|
||||
|
||||
log := p.log
|
||||
if log == nil {
|
||||
log = logger.Default()
|
||||
}
|
||||
|
||||
var options bypass.Options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
@@ -77,13 +82,13 @@ func (p *httpPlugin) Contains(ctx context.Context, network, addr string, opts ..
|
||||
}
|
||||
v, err := json.Marshal(&rb)
|
||||
if err != nil {
|
||||
p.log.Error(err)
|
||||
log.Error(err)
|
||||
return true
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, bytes.NewReader(v))
|
||||
if err != nil {
|
||||
p.log.Error(err)
|
||||
log.Error(err)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -93,7 +98,7 @@ func (p *httpPlugin) Contains(ctx context.Context, network, addr string, opts ..
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
p.log.Error(err)
|
||||
log.Error(err)
|
||||
return true
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -104,7 +109,7 @@ func (p *httpPlugin) Contains(ctx context.Context, network, addr string, opts ..
|
||||
|
||||
res := httpPluginResponse{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
p.log.Error(err)
|
||||
log.Error(err)
|
||||
return true
|
||||
}
|
||||
return res.OK
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package bypass
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-gost/core/bypass"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewHTTPPlugin(t *testing.T) {
|
||||
p := NewHTTPPlugin("test", "http://localhost:9999")
|
||||
require.NotNil(t, p)
|
||||
|
||||
hp, ok := p.(*httpPlugin)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "http://localhost:9999", hp.url)
|
||||
assert.NotNil(t, hp.client)
|
||||
assert.NotNil(t, hp.log)
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_Success(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, http.MethodPost, r.Method)
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"ok": true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := NewHTTPPlugin("test", srv.URL)
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_Deny(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"ok": false}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := NewHTTPPlugin("test", srv.URL)
|
||||
assert.False(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_Non200(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := NewHTTPPlugin("test", srv.URL)
|
||||
// Non-200 returns true (fail-open)
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_InvalidJSON(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`not json`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := NewHTTPPlugin("test", srv.URL)
|
||||
// Invalid JSON returns true (fail-open)
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_ConnectionRefused(t *testing.T) {
|
||||
p := NewHTTPPlugin("test", "http://127.0.0.1:0", plugin.TimeoutOption(0))
|
||||
// Connection refused returns true (fail-open)
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_NilClient(t *testing.T) {
|
||||
hp := &httpPlugin{
|
||||
url: "http://localhost",
|
||||
client: nil,
|
||||
}
|
||||
// Nil client returns true (fail-open)
|
||||
assert.True(t, hp.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_WithService(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"ok": true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := NewHTTPPlugin("test", srv.URL)
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1", bypass.WithService("myservice")))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_WithHostAndPath(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"ok": true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := NewHTTPPlugin("test", srv.URL)
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1",
|
||||
bypass.WithHostOption("example.com"),
|
||||
bypass.WithPathOption("/api/v1"),
|
||||
))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_WithCustomHeader(t *testing.T) {
|
||||
var receivedHeader string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedHeader = r.Header.Get("X-Custom")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"ok": true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := NewHTTPPlugin("test", srv.URL, plugin.HeaderOption(http.Header{
|
||||
"X-Custom": []string{"test-value"},
|
||||
}))
|
||||
assert.True(t, p.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
assert.Equal(t, "test-value", receivedHeader)
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_NoHeader(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"ok": true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
hp := &httpPlugin{
|
||||
url: srv.URL,
|
||||
client: plugin.NewHTTPClient(&plugin.Options{}),
|
||||
header: nil,
|
||||
}
|
||||
assert.True(t, hp.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Contains_BadURL(t *testing.T) {
|
||||
hp := &httpPlugin{
|
||||
url: "http://invalid hostname", // space causes NewRequest to fail
|
||||
client: plugin.NewHTTPClient(&plugin.Options{}),
|
||||
}
|
||||
// Bad URL returns true (fail-open)
|
||||
assert.True(t, hp.Contains(context.Background(), "tcp", "192.168.1.1"))
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Close_NilClient(t *testing.T) {
|
||||
hp := &httpPlugin{client: nil}
|
||||
err := hp.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_Close_WithClient(t *testing.T) {
|
||||
hp := &httpPlugin{
|
||||
client: plugin.NewHTTPClient(&plugin.Options{}),
|
||||
}
|
||||
err := hp.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPPlugin_IsWhitelist(t *testing.T) {
|
||||
hp := &httpPlugin{}
|
||||
assert.False(t, hp.IsWhitelist())
|
||||
}
|
||||
|
||||
var _ io.Closer = (*httpPlugin)(nil)
|
||||
Reference in New Issue
Block a user