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:
ginuerzh
2026-05-23 16:04:30 +08:00
parent bf4af78ebf
commit d015378654
4 changed files with 1201 additions and 4 deletions
+796
View File
@@ -0,0 +1,796 @@
package bypass
import (
"context"
"io"
"strings"
"testing"
"time"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/internal/loader"
xlogger "github.com/go-gost/x/logger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// --- Option tests ---
func TestWhitelistOption(t *testing.T) {
var opts options
WhitelistOption(true)(&opts)
assert.True(t, opts.whitelist)
WhitelistOption(false)(&opts)
assert.False(t, opts.whitelist)
}
func TestMatchersOption(t *testing.T) {
var opts options
MatchersOption([]string{"192.168.1.1", "10.0.0.0/8"})(&opts)
assert.Equal(t, []string{"192.168.1.1", "10.0.0.0/8"}, opts.matchers)
}
func TestReloadPeriodOption(t *testing.T) {
var opts options
ReloadPeriodOption(5 * time.Second)(&opts)
assert.Equal(t, 5*time.Second, opts.period)
}
func TestFileLoaderOption(t *testing.T) {
var opts options
fl := &mockLoader{}
FileLoaderOption(fl)(&opts)
assert.Equal(t, fl, opts.fileLoader)
}
func TestRedisLoaderOption(t *testing.T) {
var opts options
rl := &mockLoader{}
RedisLoaderOption(rl)(&opts)
assert.Equal(t, rl, opts.redisLoader)
}
func TestHTTPLoaderOption(t *testing.T) {
var opts options
hl := &mockLoader{}
HTTPLoaderOption(hl)(&opts)
assert.Equal(t, hl, opts.httpLoader)
}
func TestLoggerOption(t *testing.T) {
var opts options
l := xlogger.Nop()
LoggerOption(l)(&opts)
assert.Equal(t, l, opts.logger)
}
// newSyncedBypass creates a bypass and blocks until the initial background
// reload completes, so matchers are ready for immediate assertions.
func newSyncedBypass(opts ...Option) *localBypass {
b := NewBypass(opts...)
lb := b.(*localBypass)
lb.reload(context.Background())
return lb
}
// --- NewBypass tests ---
func TestNewBypass_Defaults(t *testing.T) {
b := newSyncedBypass()
require.NotNil(t, b)
defer b.Close()
// Blacklist mode with no rules: nothing matches, so traffic goes through proxy
assert.False(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestNewBypass_WithLogger(t *testing.T) {
b := newSyncedBypass(LoggerOption(xlogger.Nop()))
require.NotNil(t, b)
defer b.Close()
}
// --- Contains tests ---
func TestContains_EmptyAddr(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
defer b.Close()
assert.False(t, b.Contains(context.Background(), "tcp", ""))
}
func TestContains_NilReceiver(t *testing.T) {
var lb *localBypass
assert.False(t, lb.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestContains_Blacklist_Matched(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestContains_Blacklist_NotMatched(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
defer b.Close()
assert.False(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
}
func TestContains_Whitelist_Matched(t *testing.T) {
b := newSyncedBypass(
WhitelistOption(true),
MatchersOption([]string{"192.168.1.1"}),
)
defer b.Close()
assert.False(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestContains_Whitelist_NotMatched(t *testing.T) {
b := newSyncedBypass(
WhitelistOption(true),
MatchersOption([]string{"192.168.1.1"}),
)
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
}
func TestContains_StripsPort(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "192.168.1.1:8080"))
}
func TestContains_CIDRMatch(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"10.0.0.0/8"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
assert.False(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestContains_WildcardMatch(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"*.example.com"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "foo.example.com"))
assert.False(t, b.Contains(context.Background(), "tcp", "foo.other.com"))
}
func TestContains_IPRangeMatch(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1-192.168.1.10"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "192.168.1.5"))
assert.False(t, b.Contains(context.Background(), "tcp", "192.168.1.100"))
}
func TestContains_InvalidAddr(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
defer b.Close()
// Invalid address should not match, return false in blacklist mode
assert.False(t, b.Contains(context.Background(), "tcp", "invalid"))
}
func TestContains_CompositePatterns(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1", "10.0.0.0/8", "*.example.com", "172.16.0.1-172.16.0.255"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
assert.True(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
assert.True(t, b.Contains(context.Background(), "tcp", "foo.example.com"))
assert.True(t, b.Contains(context.Background(), "tcp", "172.16.0.50"))
assert.False(t, b.Contains(context.Background(), "tcp", "8.8.8.8"))
}
func TestContains_IPv6(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"fd00::1"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "fd00::1"))
assert.False(t, b.Contains(context.Background(), "tcp", "fd00::2"))
}
func TestContains_IPv6WithPort(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"::1"}))
defer b.Close()
// net.SplitHostPort handles [::1]:8080 format
assert.True(t, b.Contains(context.Background(), "tcp", "[::1]:8080"))
}
func TestContains_WildcardWithPort(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"*.example.com:8080"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "foo.example.com:8080"))
assert.False(t, b.Contains(context.Background(), "tcp", "foo.example.com:9090"))
}
// --- IsWhitelist tests ---
func TestIsWhitelist_Blacklist(t *testing.T) {
b := newSyncedBypass()
defer b.Close()
assert.False(t, b.IsWhitelist())
}
func TestIsWhitelist_Whitelist(t *testing.T) {
b := newSyncedBypass(WhitelistOption(true))
defer b.Close()
assert.True(t, b.IsWhitelist())
}
// --- parseLine tests ---
func TestParseLine_Normal(t *testing.T) {
lb := &localBypass{}
assert.Equal(t, "192.168.1.1", lb.parseLine("192.168.1.1"))
}
func TestParseLine_WithComment(t *testing.T) {
lb := &localBypass{}
assert.Equal(t, "192.168.1.1", lb.parseLine("192.168.1.1 # a comment"))
}
func TestParseLine_CommentOnly(t *testing.T) {
lb := &localBypass{}
assert.Equal(t, "", lb.parseLine("# comment only"))
}
func TestParseLine_Empty(t *testing.T) {
lb := &localBypass{}
assert.Equal(t, "", lb.parseLine(""))
}
func TestParseLine_Whitespace(t *testing.T) {
lb := &localBypass{}
assert.Equal(t, "192.168.1.1", lb.parseLine(" 192.168.1.1 "))
}
// --- parsePatterns tests ---
func TestParsePatterns_NilReader(t *testing.T) {
lb := &localBypass{}
patterns, err := lb.parsePatterns(nil)
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestParsePatterns_Normal(t *testing.T) {
lb := &localBypass{}
r := strings.NewReader("192.168.1.1\n10.0.0.1\n# comment\n\n fd00::1 \n")
patterns, err := lb.parsePatterns(r)
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1", "10.0.0.1", "fd00::1"}, patterns)
}
func TestParsePatterns_EdgeCases(t *testing.T) {
lb := &localBypass{}
r := strings.NewReader("# full comment\n\n # indented comment\n192.168.1.1 # inline")
patterns, err := lb.parsePatterns(r)
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1"}, patterns)
}
// --- matched tests ---
func TestMatched_IP(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
defer b.Close()
assert.True(t, b.matched("192.168.1.1"))
assert.False(t, b.matched("10.0.0.1"))
}
func TestMatched_CIDR(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"10.0.0.0/8"}))
defer b.Close()
assert.True(t, b.matched("10.0.0.1"))
assert.False(t, b.matched("192.168.1.1"))
}
func TestMatched_Wildcard(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"*.example.com"}))
defer b.Close()
assert.True(t, b.matched("foo.example.com"))
assert.False(t, b.matched("foo.other.com"))
}
func TestMatched_IPRange(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"10.0.0.1-10.0.0.100"}))
defer b.Close()
assert.True(t, b.matched("10.0.0.50"))
assert.False(t, b.matched("10.0.0.200"))
}
// --- reload tests ---
func TestReload_OnlyIPs(t *testing.T) {
lb := &localBypass{
options: options{
matchers: []string{"1.1.1.1", "8.8.8.8"},
},
logger: xlogger.Nop(),
}
err := lb.reload(context.Background())
assert.NoError(t, err)
assert.True(t, lb.matched("1.1.1.1"))
assert.True(t, lb.matched("8.8.8.8"))
assert.False(t, lb.matched("9.9.9.9"))
}
func TestReload_OnlyCIDRs(t *testing.T) {
lb := &localBypass{
options: options{
matchers: []string{"192.168.0.0/16", "10.0.0.0/8"},
},
logger: xlogger.Nop(),
}
err := lb.reload(context.Background())
assert.NoError(t, err)
assert.True(t, lb.matched("192.168.1.1"))
assert.True(t, lb.matched("10.0.0.1"))
assert.False(t, lb.matched("172.16.0.1"))
}
func TestReload_OnlyWildcards(t *testing.T) {
lb := &localBypass{
options: options{
matchers: []string{"*.example.com", "*.test.org"},
},
logger: xlogger.Nop(),
}
err := lb.reload(context.Background())
assert.NoError(t, err)
assert.True(t, lb.matched("foo.example.com"))
assert.True(t, lb.matched("bar.test.org"))
assert.False(t, lb.matched("other.com"))
}
func TestReload_OnlyIPRanges(t *testing.T) {
lb := &localBypass{
options: options{
matchers: []string{"192.168.1.1-192.168.1.50"},
},
logger: xlogger.Nop(),
}
err := lb.reload(context.Background())
assert.NoError(t, err)
assert.True(t, lb.matched("192.168.1.25"))
assert.False(t, lb.matched("192.168.1.100"))
}
// --- load tests ---
func TestLoad_AllNilLoaders(t *testing.T) {
lb := &localBypass{
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_FileLoaderWithList(t *testing.T) {
lb := &localBypass{
options: options{
fileLoader: &mockListerLoader{list: []string{"192.168.1.1", "# comment", "10.0.0.1"}},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1", "10.0.0.1"}, patterns)
}
func TestLoad_FileLoaderWithLoad(t *testing.T) {
lb := &localBypass{
options: options{
fileLoader: &mockLoader{data: "192.168.1.1\n10.0.0.1\n"},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1", "10.0.0.1"}, patterns)
}
func TestLoad_RedisLoaderWithList(t *testing.T) {
lb := &localBypass{
options: options{
redisLoader: &mockListerLoader{list: []string{"redis-host-1", "redis-host-2"}},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"redis-host-1", "redis-host-2"}, patterns)
}
func TestLoad_RedisLoaderWithLoad(t *testing.T) {
lb := &localBypass{
options: options{
redisLoader: &mockLoader{data: "192.168.2.1\n"},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.2.1"}, patterns)
}
func TestLoad_HTTPLoader(t *testing.T) {
lb := &localBypass{
options: options{
httpLoader: &mockLoader{data: "10.10.10.1\n10.10.10.2\n"},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"10.10.10.1", "10.10.10.2"}, patterns)
}
func TestLoad_FileLoaderWithListError(t *testing.T) {
lb := &localBypass{
options: options{
fileLoader: &mockListerLoader{listErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_FileLoaderWithLoadError(t *testing.T) {
lb := &localBypass{
options: options{
fileLoader: &mockLoader{loadErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_RedisLoaderWithListError(t *testing.T) {
lb := &localBypass{
options: options{
redisLoader: &mockListerLoader{listErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_RedisLoaderWithLoadError(t *testing.T) {
lb := &localBypass{
options: options{
redisLoader: &mockLoader{loadErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_HTTPLoaderError(t *testing.T) {
lb := &localBypass{
options: options{
httpLoader: &mockLoader{loadErr: io.ErrUnexpectedEOF},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Nil(t, patterns)
}
func TestLoad_FileLoaderWithListParsing(t *testing.T) {
lb := &localBypass{
options: options{
fileLoader: &mockListerLoader{list: []string{" # comment", "", " 192.168.1.1 "}},
},
logger: xlogger.Nop(),
}
patterns, err := lb.load(context.Background())
assert.NoError(t, err)
assert.Equal(t, []string{"192.168.1.1"}, patterns)
}
// --- periodReload tests ---
func TestPeriodReload_ZeroPeriod(t *testing.T) {
lb := &localBypass{
logger: xlogger.Nop(),
options: options{period: 0},
}
err := lb.periodReload(context.Background())
assert.NoError(t, err)
}
func TestPeriodReload_NegativePeriod(t *testing.T) {
lb := &localBypass{
logger: xlogger.Nop(),
options: options{period: -1},
}
err := lb.periodReload(context.Background())
assert.NoError(t, err)
}
func TestPeriodReload_WithContextCancel(t *testing.T) {
lb := &localBypass{
logger: xlogger.Nop(),
options: options{period: 500 * time.Millisecond},
}
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
go func() {
errCh <- lb.periodReload(ctx)
}()
time.Sleep(1100 * time.Millisecond)
cancel()
select {
case err := <-errCh:
assert.Equal(t, context.Canceled, err)
case <-time.After(3 * time.Second):
t.Fatal("periodReload did not return after context cancel")
}
}
// --- Close tests ---
func TestClose_NoLoaders(t *testing.T) {
b := newSyncedBypass()
err := b.Close()
assert.NoError(t, err)
}
func TestClose_WithAllLoaders(t *testing.T) {
fl := &mockLoader{}
rl := &mockLoader{}
hl := &mockLoader{}
b := newSyncedBypass(
FileLoaderOption(fl),
RedisLoaderOption(rl),
HTTPLoaderOption(hl),
)
err := b.Close()
assert.NoError(t, err)
assert.True(t, fl.closed)
assert.True(t, rl.closed)
assert.True(t, hl.closed)
}
// --- BypassGroup tests ---
func TestBypassGroup_Empty(t *testing.T) {
g := BypassGroup()
assert.False(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_AllBlacklistAllMatch(t *testing.T) {
g := BypassGroup(
alwaysContains{},
alwaysContains{},
)
assert.True(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_BlacklistOneMatches(t *testing.T) {
g := BypassGroup(
alwaysContains{},
neverContains{},
)
// OR logic for blacklist: any match triggers bypass
assert.True(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_BlacklistNoneMatches(t *testing.T) {
g := BypassGroup(
neverContains{},
neverContains{},
)
assert.False(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_WhitelistAllMatch(t *testing.T) {
// AND logic for whitelist: all must match
g := BypassGroup(
alwaysContainsWhitelist{},
alwaysContainsWhitelist{},
)
assert.True(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_WhitelistOneFails(t *testing.T) {
// AND logic for whitelist: if any fails, combined result is false
g := BypassGroup(
alwaysContainsWhitelist{},
neverContainsWhitelist{},
)
assert.False(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_MixedWhitelsBlacklist(t *testing.T) {
// Whitelist all match, blacklist irrelevant
g := BypassGroup(
alwaysContainsWhitelist{},
alwaysContains{}, // blacklist — should be ignored if whitelist all pass
)
// All whitelists match → status=true → blacklist skipped
assert.True(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_MixedWhitelistFailsBlacklistMatches(t *testing.T) {
// Whitelist fails → fallthrough to blacklist which matches
g := BypassGroup(
neverContainsWhitelist{},
alwaysContains{},
)
assert.True(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_MixedWhitelistFailsBlacklistNoMatch(t *testing.T) {
g := BypassGroup(
neverContainsWhitelist{},
neverContains{},
)
assert.False(t, g.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestBypassGroup_IsWhitelist(t *testing.T) {
g := BypassGroup()
assert.False(t, g.IsWhitelist())
}
// --- NewBypass reload error in background ---
func TestNewBypass_ReloadError(t *testing.T) {
fl := &mockLoader{loadErr: io.ErrUnexpectedEOF}
b := newSyncedBypass(
FileLoaderOption(fl),
LoggerOption(xlogger.Nop()),
)
defer b.Close()
// Should still work, just logged a warning
assert.False(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
}
// --- Network parameter ignored ---
func TestContains_NetworkIgnoredForMatching(t *testing.T) {
b := newSyncedBypass(MatchersOption([]string{"192.168.1.1"}))
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
assert.True(t, b.Contains(context.Background(), "udp", "192.168.1.1"))
assert.True(t, b.Contains(context.Background(), "ip4", "192.168.1.1"))
}
// --- Debug logging doesn't panic ---
func TestContains_DenyLogs(t *testing.T) {
b := newSyncedBypass(
MatchersOption([]string{"192.168.1.1"}),
LoggerOption(xlogger.Nop()),
)
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "192.168.1.1"))
}
func TestContains_WhitelistDenyLogs(t *testing.T) {
b := newSyncedBypass(
WhitelistOption(true),
MatchersOption([]string{"192.168.1.1"}),
LoggerOption(xlogger.Nop()),
)
defer b.Close()
assert.True(t, b.Contains(context.Background(), "tcp", "10.0.0.1"))
}
// --- Mock types ---
type mockLoader struct {
data string
loadErr error
closed bool
}
func (m *mockLoader) Load(ctx context.Context) (io.Reader, error) {
if m.loadErr != nil {
return nil, m.loadErr
}
return strings.NewReader(m.data), nil
}
func (m *mockLoader) Close() error {
m.closed = true
return nil
}
type mockListerLoader struct {
list []string
listErr error
}
func (m *mockListerLoader) Load(ctx context.Context) (io.Reader, error) {
return strings.NewReader(strings.Join(m.list, "\n")), nil
}
func (m *mockListerLoader) List(ctx context.Context) ([]string, error) {
if m.listErr != nil {
return nil, m.listErr
}
return m.list, nil
}
func (m *mockListerLoader) Close() error {
return nil
}
type alwaysContains struct{}
func (alwaysContains) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
return true
}
func (alwaysContains) IsWhitelist() bool { return false }
type neverContains struct{}
func (neverContains) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
return false
}
func (neverContains) IsWhitelist() bool { return false }
type alwaysContainsWhitelist struct{}
func (alwaysContainsWhitelist) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
return true
}
func (alwaysContainsWhitelist) IsWhitelist() bool { return true }
type neverContainsWhitelist struct{}
func (neverContainsWhitelist) Contains(ctx context.Context, network, addr string, opts ...bypass.Option) bool {
return false
}
func (neverContainsWhitelist) IsWhitelist() bool { return true }
// Ensure mock types implement the interfaces
var _ loader.Loader = (*mockLoader)(nil)
var _ loader.Lister = (*mockListerLoader)(nil)
var _ loader.Loader = (*mockListerLoader)(nil)
var _ bypass.Bypass = alwaysContains{}
var _ bypass.Bypass = neverContains{}
var _ bypass.Bypass = alwaysContainsWhitelist{}
var _ bypass.Bypass = neverContainsWhitelist{}
var _ bypass.Bypass = (*localBypass)(nil)
var _ bypass.Bypass = (*bypassGroup)(nil)
var _ logger.Logger = xlogger.Nop()
+222
View File
@@ -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)
+9 -4
View File
@@ -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
+174
View File
@@ -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)