Files
x/auth/auth_test.go
T
ginuerzh 143438a30e feat(auth): add skipauth metadata option to bypass auth for whitelisted client IPs
Introduces WhitelistedAuthenticator that wraps an auth.Authenticator to skip
authentication when the client IP matches configured CIDR ranges or exact IPs.
The wrapping happens at service-parse time, transparently covering all handler
types (HTTP, SOCKS4/5, relay, http2, SSH).

Usage:
  gost -L 'user:pass@:5999?skipauth=192.168.0.0/24,172.22.22.22/32'
2026-06-27 14:33:40 +08:00

1101 lines
29 KiB
Go

package auth
import (
"context"
"errors"
"io"
"net"
"strings"
"testing"
"time"
"github.com/go-gost/core/auth"
xctx "github.com/go-gost/x/ctx"
"github.com/go-gost/x/internal/loader"
xlogger "github.com/go-gost/x/logger"
)
// --- mock types ---
type mockLoader struct {
loadData 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.loadData), nil
}
func (m *mockLoader) Close() error {
m.closed = true
return nil
}
type mockMapper struct {
mapData map[string]string
mapErr error
closed bool
}
func (m *mockMapper) Load(ctx context.Context) (io.Reader, error) {
return nil, nil
}
func (m *mockMapper) Map(ctx context.Context) (map[string]string, error) {
if m.mapErr != nil {
return nil, m.mapErr
}
return m.mapData, nil
}
func (m *mockMapper) Close() error {
m.closed = true
return nil
}
var _ loader.Loader = (*mockLoader)(nil)
var _ loader.Loader = (*mockMapper)(nil)
var _ loader.Mapper = (*mockMapper)(nil)
// --- option tests ---
func TestAuthsOption(t *testing.T) {
opts := &options{}
auths := map[string]string{"user": "pass"}
AuthsOption(auths)(opts)
if len(opts.auths) != 1 || opts.auths["user"] != "pass" {
t.Fatalf("expected auths to be set, got %v", opts.auths)
}
}
func TestReloadPeriodOption(t *testing.T) {
opts := &options{}
ReloadPeriodOption(5 * time.Second)(opts)
if opts.period != 5*time.Second {
t.Fatalf("expected period 5s, got %v", opts.period)
}
}
func TestFileLoaderOption(t *testing.T) {
opts := &options{}
ml := &mockLoader{}
FileLoaderOption(ml)(opts)
if opts.fileLoader != ml {
t.Fatal("expected fileLoader to be set")
}
}
func TestRedisLoaderOption(t *testing.T) {
opts := &options{}
ml := &mockLoader{}
RedisLoaderOption(ml)(opts)
if opts.redisLoader != ml {
t.Fatal("expected redisLoader to be set")
}
}
func TestHTTPLoaderOption(t *testing.T) {
opts := &options{}
ml := &mockLoader{}
HTTPLoaderOption(ml)(opts)
if opts.httpLoader != ml {
t.Fatal("expected httpLoader to be set")
}
}
func TestLoggerOption(t *testing.T) {
opts := &options{}
l := xlogger.Nop()
LoggerOption(l)(opts)
if opts.logger != l {
t.Fatal("expected logger to be set")
}
}
// --- NewAuthenticator tests ---
func TestNewAuthenticator_Defaults(t *testing.T) {
p := NewAuthenticator()
if p == nil {
t.Fatal("expected authenticator to be created")
}
defer p.(*authenticator).Close()
ap := p.(*authenticator)
if ap.logger == nil {
t.Fatal("expected non-nil logger (Nop fallback)")
}
if ap.cancelFunc == nil {
t.Fatal("expected cancelFunc to be set")
}
if len(ap.kvs) != 0 {
t.Fatal("expected empty kvs")
}
}
func TestNewAuthenticator_WithAuths(t *testing.T) {
p := NewAuthenticator(AuthsOption(map[string]string{"u": "p"}))
defer p.(*authenticator).Close()
// Initial reload is async — wait for it.
time.Sleep(50 * time.Millisecond)
ap := p.(*authenticator)
ap.mu.RLock()
if ap.kvs["u"] != "p" {
t.Fatal("expected kvs to contain u=p")
}
ap.mu.RUnlock()
}
func TestNewAuthenticator_WithLogger(t *testing.T) {
l := xlogger.Nop()
p := NewAuthenticator(LoggerOption(l))
defer p.(*authenticator).Close()
ap := p.(*authenticator)
if ap.logger != l {
t.Fatal("expected custom logger")
}
}
// --- Authenticate tests ---
func TestAuthenticate_NilReceiver(t *testing.T) {
var p *authenticator
id, ok := p.Authenticate(context.Background(), "u", "p")
if !ok || id != "" {
t.Fatalf("nil receiver should return '', true, got %q, %v", id, ok)
}
}
func TestAuthenticate_EmptyKVs(t *testing.T) {
p := NewAuthenticator()
defer p.(*authenticator).Close()
id, ok := p.Authenticate(context.Background(), "u", "p")
if ok || id != "" {
t.Fatalf("empty kvs should return '', false, got %q, %v", id, ok)
}
}
func TestAuthenticate_UserNotFound(t *testing.T) {
p := NewAuthenticator(AuthsOption(map[string]string{"admin": "secret"}))
defer p.(*authenticator).Close()
// Wait for initial reload to populate kvs
time.Sleep(50 * time.Millisecond)
// When user is not in kvs, Authenticate returns (user, false)
id, ok := p.Authenticate(context.Background(), "nobody", "")
if ok || id != "nobody" {
t.Fatalf("unknown user should return 'nobody', false, got %q, %v", id, ok)
}
}
func TestAuthenticate_UserFoundNoPassword(t *testing.T) {
p := NewAuthenticator(AuthsOption(map[string]string{"open": ""}))
defer p.(*authenticator).Close()
time.Sleep(50 * time.Millisecond)
id, ok := p.Authenticate(context.Background(), "open", "")
if !ok || id != "open" {
t.Fatalf("expected open, true, got %q, %v", id, ok)
}
id2, ok2 := p.Authenticate(context.Background(), "open", "anything")
if !ok2 || id2 != "open" {
t.Fatalf("any password should pass for empty-password user, got %q, %v", id2, ok2)
}
}
func TestAuthenticate_PasswordMatch(t *testing.T) {
p := NewAuthenticator(AuthsOption(map[string]string{"admin": "secret"}))
defer p.(*authenticator).Close()
time.Sleep(50 * time.Millisecond)
id, ok := p.Authenticate(context.Background(), "admin", "secret")
if !ok || id != "admin" {
t.Fatalf("expected admin, true, got %q, %v", id, ok)
}
}
func TestAuthenticate_PasswordMismatch(t *testing.T) {
p := NewAuthenticator(AuthsOption(map[string]string{"admin": "secret"}))
defer p.(*authenticator).Close()
time.Sleep(50 * time.Millisecond)
id, ok := p.Authenticate(context.Background(), "admin", "wrong")
if ok {
t.Fatalf("wrong password should return false, got %q, %v", id, ok)
}
}
// --- parseAuths tests ---
func TestParseAuths_NilReader(t *testing.T) {
p := &authenticator{}
result, err := p.parseAuths(nil)
if err != nil || result != nil {
t.Fatalf("nil reader should return nil, nil, got %v, %v", result, err)
}
}
func TestParseAuths_EmptyReader(t *testing.T) {
p := &authenticator{}
result, err := p.parseAuths(strings.NewReader(""))
if err != nil || len(result) != 0 {
t.Fatalf("empty reader should return empty map, got %v, %v", result, err)
}
}
func TestParseAuths_Comments(t *testing.T) {
p := &authenticator{}
result, err := p.parseAuths(strings.NewReader("# this is a comment\n# another comment"))
if err != nil || len(result) != 0 {
t.Fatalf("comments should be skipped, got %v, %v", result, err)
}
}
func TestParseAuths_SingleKey(t *testing.T) {
p := &authenticator{}
result, err := p.parseAuths(strings.NewReader("testuser"))
if err != nil {
t.Fatal(err)
}
if result["testuser"] != "" {
t.Fatalf("single key should have empty password, got %q", result["testuser"])
}
}
func TestParseAuths_KeyValue(t *testing.T) {
p := &authenticator{}
result, err := p.parseAuths(strings.NewReader("testuser testpass"))
if err != nil {
t.Fatal(err)
}
if result["testuser"] != "testpass" {
t.Fatalf("expected testpass, got %q", result["testuser"])
}
}
func TestParseAuths_TabConversion(t *testing.T) {
p := &authenticator{}
result, err := p.parseAuths(strings.NewReader("testuser\ttestpass"))
if err != nil {
t.Fatal(err)
}
if result["testuser"] != "testpass" {
t.Fatalf("tab should be converted to space, got %q", result["testuser"])
}
}
func TestParseAuths_MultipleLines(t *testing.T) {
p := &authenticator{}
data := "user1 pass1\nuser2\nuser3 pass3\n# comment\n\n user4 pass4 "
result, err := p.parseAuths(strings.NewReader(data))
if err != nil {
t.Fatal(err)
}
if len(result) != 4 {
t.Fatalf("expected 4 entries, got %d", len(result))
}
if result["user1"] != "pass1" {
t.Fatalf("user1: expected pass1, got %q", result["user1"])
}
if result["user2"] != "" {
t.Fatalf("user2: expected empty, got %q", result["user2"])
}
if result["user3"] != "pass3" {
t.Fatalf("user3: expected pass3, got %q", result["user3"])
}
if result["user4"] != "pass4" {
t.Fatalf("user4: expected pass4, got %q", result["user4"])
}
}
func TestParseAuths_EmptyKeyInPair(t *testing.T) {
p := &authenticator{}
result, err := p.parseAuths(strings.NewReader(" extra spaces here"))
if err != nil {
t.Fatal(err)
}
if result["extra"] != "spaces here" {
t.Fatalf("expected 'spaces here', got %q", result["extra"])
}
}
func TestParseAuths_WhitespaceOnlyLines(t *testing.T) {
// TrimSpace removes leading/trailing whitespace before SplitN,
// so whitespace-only lines become empty and produce no entries.
p := &authenticator{}
result, err := p.parseAuths(strings.NewReader(" \n\t "))
if err != nil || len(result) != 0 {
t.Fatalf("whitespace-only lines should produce empty map, got %v, %v", result, err)
}
}
// --- load tests ---
func TestLoad_NoLoaders(t *testing.T) {
p := &authenticator{logger: xlogger.Nop()}
m, err := p.load(context.Background())
if err != nil || len(m) != 0 {
t.Fatalf("expected empty map, nil err, got %v, %v", m, err)
}
}
func TestLoad_FileLoaderMapper_Success(t *testing.T) {
p := &authenticator{
options: options{
fileLoader: &mockMapper{mapData: map[string]string{"a": "b"}},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err != nil || m["a"] != "b" {
t.Fatalf("expected a=b, got %v, %v", m, err)
}
}
func TestLoad_FileLoaderMapper_Error(t *testing.T) {
p := &authenticator{
options: options{
fileLoader: &mockMapper{mapErr: errors.New("map error")},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err == nil {
t.Fatal("expected error from mapper")
}
}
func TestLoad_FileLoaderNonMapper_Success(t *testing.T) {
p := &authenticator{
options: options{
fileLoader: &mockLoader{loadData: "u1 p1"},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err != nil || m["u1"] != "p1" {
t.Fatalf("expected u1=p1, got %v, %v", m, err)
}
}
func TestLoad_FileLoaderNonMapper_LoadError(t *testing.T) {
p := &authenticator{
options: options{
fileLoader: &mockLoader{loadErr: errors.New("load error")},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err == nil {
t.Fatal("expected error from loader")
}
}
func TestLoad_FileLoaderNonMapper_EmptyData(t *testing.T) {
// mockLoader with empty loadData returns empty string reader;
// parseAuths succeeds with empty result (not a parse error).
p := &authenticator{
options: options{
fileLoader: &mockLoader{},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err != nil || len(m) != 0 {
t.Fatalf("expected empty map, got %v, %v", m, err)
}
}
func TestLoad_RedisLoaderMapper_Success(t *testing.T) {
p := &authenticator{
options: options{
redisLoader: &mockMapper{mapData: map[string]string{"r": "s"}},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err != nil || m["r"] != "s" {
t.Fatalf("expected r=s, got %v, %v", m, err)
}
}
func TestLoad_RedisLoaderMapper_Error(t *testing.T) {
p := &authenticator{
options: options{
redisLoader: &mockMapper{mapErr: errors.New("redis error")},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err == nil {
t.Fatal("expected error from redis mapper")
}
}
func TestLoad_RedisLoaderNonMapper_Success(t *testing.T) {
p := &authenticator{
options: options{
redisLoader: &mockLoader{loadData: "r1 rp1"},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err != nil || m["r1"] != "rp1" {
t.Fatalf("expected r1=rp1, got %v, %v", m, err)
}
}
func TestLoad_RedisLoaderNonMapper_LoadError(t *testing.T) {
p := &authenticator{
options: options{
redisLoader: &mockLoader{loadErr: errors.New("redis load err")},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err == nil {
t.Fatal("expected error")
}
}
func TestLoad_RedisLoaderNonMapper_ParseError(t *testing.T) {
p := &authenticator{
options: options{
redisLoader: &mockLoader{loadData: "# comment"},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
// parseAuths succeeds (comments are skipped), just empty
if err != nil || len(m) != 0 {
t.Fatalf("expected empty map, got %v, %v", m, err)
}
}
func TestLoad_HTTPLoader_Success(t *testing.T) {
p := &authenticator{
options: options{
httpLoader: &mockLoader{loadData: "h1 hp1"},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err != nil || m["h1"] != "hp1" {
t.Fatalf("expected h1=hp1, got %v, %v", m, err)
}
}
func TestLoad_HTTPLoader_LoadError(t *testing.T) {
p := &authenticator{
options: options{
httpLoader: &mockLoader{loadErr: errors.New("http error")},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err == nil {
t.Fatal("expected error from http loader")
}
}
func TestLoad_HTTPLoader_ParseError(t *testing.T) {
p := &authenticator{
options: options{
httpLoader: &mockLoader{loadData: "# comment"},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err != nil || len(m) != 0 {
t.Fatalf("expected empty map, got %v, %v", m, err)
}
}
func TestLoad_MultipleLoaders_Merge(t *testing.T) {
p := &authenticator{
options: options{
fileLoader: &mockLoader{loadData: "f1 fp1"},
redisLoader: &mockLoader{loadData: "r1 rp1"},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err != nil {
t.Fatal(err)
}
if m["f1"] != "fp1" || m["r1"] != "rp1" {
t.Fatalf("expected merged results, got %v", m)
}
}
func TestLoad_FirstSuccessSecondError(t *testing.T) {
// fileLoader succeeds, redisLoader fails — error from redisLoader is returned
// but fileLoader results are preserved
p := &authenticator{
options: options{
fileLoader: &mockLoader{loadData: "f1 fp1"},
redisLoader: &mockLoader{loadErr: errors.New("redis fail")},
},
logger: xlogger.Nop(),
}
m, err := p.load(context.Background())
if err == nil {
t.Fatal("expected error from redis loader")
}
if m["f1"] != "fp1" {
t.Fatalf("file loader results should be preserved, got %v", m)
}
}
func TestLoad_RedisMapper_ErrorConditionalLoadErr(t *testing.T) {
// redisLoader mapper error when loadErr is already set (from fileLoader error)
p := &authenticator{
options: options{
fileLoader: &mockLoader{loadErr: errors.New("file fail")},
redisLoader: &mockMapper{mapErr: errors.New("redis fail")},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
// loadErr was set by fileLoader, redisLoader error doesn't overwrite it
if err == nil || err.Error() != "file fail" {
t.Fatalf("expected 'file fail', got %v", err)
}
}
func TestLoad_HTTPLoader_ErrorConditionalLoadErr(t *testing.T) {
// httpLoader error when loadErr already set
p := &authenticator{
options: options{
fileLoader: &mockLoader{loadErr: errors.New("file fail")},
httpLoader: &mockLoader{loadErr: errors.New("http fail")},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err == nil || err.Error() != "file fail" {
t.Fatalf("expected 'file fail' (first error preserved), got %v", err)
}
}
func TestLoad_FileMapper_ErrorConditionalLoadErr(t *testing.T) {
// fileLoader mapper error, then redisLoader non-mapper error
// fileLoader mapper sets loadErr, redisLoader non-mapper doesn't overwrite (conditional)
p := &authenticator{
options: options{
fileLoader: &mockMapper{mapErr: errors.New("file map fail")},
redisLoader: &mockLoader{loadErr: errors.New("redis fail")},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err == nil || err.Error() != "file map fail" {
t.Fatalf("expected 'file map fail', got %v", err)
}
}
// --- reload tests ---
func TestReload_Success(t *testing.T) {
p := &authenticator{
options: options{
auths: map[string]string{"static": "val"},
},
kvs: make(map[string]string),
logger: xlogger.Nop(),
}
err := p.reload(context.Background())
if err != nil || len(p.kvs) != 1 || p.kvs["static"] != "val" {
t.Fatalf("expected static=val, got %v, err=%v", p.kvs, err)
}
}
func TestReload_LoadError(t *testing.T) {
p := &authenticator{
options: options{
fileLoader: &mockLoader{loadErr: errors.New("fail")},
},
kvs: map[string]string{"old": "data"},
logger: xlogger.Nop(),
}
err := p.reload(context.Background())
if err == nil {
t.Fatal("expected error from reload")
}
// On error, kvs should not be updated (old data preserved)
if p.kvs["old"] != "data" {
t.Fatal("old kvs should be preserved on reload error")
}
}
func TestReload_ClearsOldData(t *testing.T) {
p := &authenticator{
options: options{
auths: map[string]string{"new": "val"},
},
kvs: map[string]string{"old": "data"},
logger: xlogger.Nop(),
}
err := p.reload(context.Background())
if err != nil {
t.Fatal(err)
}
if _, exists := p.kvs["old"]; exists {
t.Fatal("old data should be cleared on successful reload")
}
if p.kvs["new"] != "val" {
t.Fatal("new data should be present")
}
}
// --- periodReload tests ---
func TestPeriodReload_NoPeriod(t *testing.T) {
p := &authenticator{
options: options{
auths: map[string]string{"k": "v"},
},
kvs: make(map[string]string),
logger: xlogger.Nop(),
}
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
err := p.periodReload(ctx)
if err != nil {
t.Fatalf("expected nil error for zero period, got %v", err)
}
// With period <= 0, the initial reload happened and then returned
if p.kvs["k"] != "v" {
t.Fatal("expected k=v after initial reload")
}
}
func TestPeriodReload_ShortPeriod_Clamped(t *testing.T) {
p := &authenticator{
options: options{
period: 500 * time.Millisecond, // < 1s, gets clamped to 1s
},
kvs: make(map[string]string),
logger: xlogger.Nop(),
}
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Initial reload happens, then ticker starts but ctx cancels before first tick
err := p.periodReload(ctx)
if err == nil {
t.Fatal("expected context error")
}
}
func TestPeriodReload_ContextCancellation(t *testing.T) {
p := &authenticator{
options: options{
period: 10 * time.Second, // long period so tick doesn't fire
},
kvs: make(map[string]string),
logger: xlogger.Nop(),
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() {
done <- p.periodReload(ctx)
}()
// Give it time to complete the initial reload and enter the loop
time.Sleep(50 * time.Millisecond)
cancel()
err := <-done
if err == nil {
t.Fatal("expected context error")
}
}
func TestPeriodReload_ReloadError_Initial(t *testing.T) {
p := &authenticator{
options: options{
fileLoader: &mockLoader{loadErr: errors.New("fail")},
period: 0, // no period, just initial reload
},
kvs: make(map[string]string),
logger: xlogger.Nop(),
}
err := p.periodReload(context.Background())
// period is 0, so after initial reload it returns nil
if err != nil {
t.Fatalf("period 0 should return nil, got %v", err)
}
}
// --- Close tests ---
func TestClose_NoLoaders(t *testing.T) {
p := NewAuthenticator()
err := p.(*authenticator).Close()
if err != nil {
t.Fatal(err)
}
}
func TestClose_WithLoaders(t *testing.T) {
fl := &mockLoader{}
rl := &mockLoader{}
hl := &mockLoader{}
p := NewAuthenticator(
FileLoaderOption(fl),
RedisLoaderOption(rl),
HTTPLoaderOption(hl),
)
err := p.(*authenticator).Close()
if err != nil {
t.Fatal(err)
}
if !fl.closed || !rl.closed || !hl.closed {
t.Fatal("all loaders should be closed")
}
}
func TestClose_CallsCancelFunc(t *testing.T) {
p := &authenticator{
kvs: make(map[string]string),
logger: xlogger.Nop(),
}
ctx, cancel := context.WithCancel(context.Background())
p.cancelFunc = cancel
err := p.Close()
if err != nil {
t.Fatal(err)
}
// Verify context was cancelled
select {
case <-ctx.Done():
// expected
default:
t.Fatal("context should be cancelled after Close")
}
}
// --- AuthenticatorGroup tests ---
func TestAuthenticatorGroup_Empty(t *testing.T) {
g := AuthenticatorGroup()
id, ok := g.Authenticate(context.Background(), "u", "p")
if ok || id != "" {
t.Fatalf("empty group should return '', false, got %q, %v", id, ok)
}
}
func TestAuthenticatorGroup_SingleSuccess(t *testing.T) {
g := AuthenticatorGroup(&staticAuther{id: "user1", ok: true})
id, ok := g.Authenticate(context.Background(), "u", "p")
if !ok || id != "user1" {
t.Fatalf("expected user1, true, got %q, %v", id, ok)
}
}
func TestAuthenticatorGroup_SingleFail(t *testing.T) {
g := AuthenticatorGroup(&staticAuther{id: "", ok: false})
id, ok := g.Authenticate(context.Background(), "u", "p")
if ok || id != "" {
t.Fatalf("expected '', false, got %q, %v", id, ok)
}
}
func TestAuthenticatorGroup_Fallthrough(t *testing.T) {
g := AuthenticatorGroup(
&staticAuther{id: "", ok: false},
&staticAuther{id: "second", ok: true},
)
id, ok := g.Authenticate(context.Background(), "u", "p")
if !ok || id != "second" {
t.Fatalf("expected second, true, got %q, %v", id, ok)
}
}
func TestAuthenticatorGroup_NilEntries(t *testing.T) {
g := AuthenticatorGroup(
nil,
&staticAuther{id: "third", ok: true},
)
id, ok := g.Authenticate(context.Background(), "u", "p")
if !ok || id != "third" {
t.Fatalf("nil entries should be skipped, expected third, true, got %q, %v", id, ok)
}
}
func TestAuthenticatorGroup_AllFail(t *testing.T) {
g := AuthenticatorGroup(
&staticAuther{id: "", ok: false},
&staticAuther{id: "", ok: false},
)
id, ok := g.Authenticate(context.Background(), "u", "p")
if ok || id != "" {
t.Fatalf("all fail should return '', false, got %q, %v", id, ok)
}
}
func TestAuthenticatorGroup_Creation(t *testing.T) {
a1 := &staticAuther{id: "a", ok: true}
a2 := &staticAuther{id: "b", ok: false}
g := AuthenticatorGroup(a1, a2).(*authenticatorGroup)
if len(g.authers) != 2 {
t.Fatalf("expected 2 authers, got %d", len(g.authers))
}
}
// --- scanner error tests ---
type errorReader struct{}
func (e *errorReader) Read(p []byte) (int, error) {
return 0, errors.New("read error")
}
func TestParseAuths_ScannerError(t *testing.T) {
p := &authenticator{}
_, err := p.parseAuths(&errorReader{})
if err == nil {
t.Fatal("expected scanner error")
}
}
// --- load parse error branch tests ---
func TestLoad_FileLoaderNonMapper_ParseErrorBranch(t *testing.T) {
// File loader non-mapper: Load succeeds but reader triggers scanner error in parseAuths.
// This hits the "file loader parse" Warnf branch.
// parse errors are NOT propagated as loadErr — only load errors are.
p := &authenticator{
options: options{
fileLoader: &errorLoader{reader: &errorReader{}},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err != nil {
t.Fatalf("parse error should not propagate, got %v", err)
}
}
func TestLoad_RedisLoaderNonMapper_ParseErrorBranch(t *testing.T) {
// Redis loader non-mapper — parseAuths returns scanner error.
p := &authenticator{
options: options{
redisLoader: &errorLoader{reader: &errorReader{}},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err != nil {
t.Fatalf("parse error should not propagate, got %v", err)
}
}
func TestLoad_HTTPLoader_ParseErrorBranch(t *testing.T) {
// HTTP loader — parseAuths returns scanner error.
p := &authenticator{
options: options{
httpLoader: &errorLoader{reader: &errorReader{}},
},
logger: xlogger.Nop(),
}
_, err := p.load(context.Background())
if err != nil {
t.Fatalf("parse error should not propagate, got %v", err)
}
}
type errorLoader struct {
reader io.Reader
}
func (l *errorLoader) Load(ctx context.Context) (io.Reader, error) {
return l.reader, nil
}
func (l *errorLoader) Close() error { return nil }
var _ loader.Loader = (*errorLoader)(nil)
// --- periodReload ticker fire test ---
func TestPeriodReload_TickerFires(t *testing.T) {
// Set up with a loader that succeeds on initial reload, so we enter the tick loop.
// Use period=1s so ticker fires quickly.
p := &authenticator{
options: options{
period: 1 * time.Second,
fileLoader: &mockLoader{loadData: "k v"},
},
kvs: make(map[string]string),
logger: xlogger.Nop(),
}
ctx, cancel := context.WithTimeout(context.Background(), 1100*time.Millisecond)
defer cancel()
done := make(chan error, 1)
go func() {
done <- p.periodReload(ctx)
}()
err := <-done
if err == nil {
t.Fatal("expected context deadline exceeded")
}
// kvs should be populated by the initial reload
p.mu.RLock()
v := p.kvs["k"]
p.mu.RUnlock()
if v != "v" {
t.Fatalf("expected k=v after initial reload, got %q", v)
}
}
func TestPeriodReload_TickerReloadError(t *testing.T) {
// Loader that fails — covers the Warnf inside the ticker.C case.
// Initial reload fails (Warnf line 116), ticker fires, reload fails again (Warnf line 133).
p := &authenticator{
options: options{
period: 1 * time.Second,
fileLoader: &mockLoader{loadErr: errors.New("persistent fail")},
},
kvs: make(map[string]string),
logger: xlogger.Nop(),
}
ctx, cancel := context.WithTimeout(context.Background(), 1100*time.Millisecond)
defer cancel()
done := make(chan error, 1)
go func() {
done <- p.periodReload(ctx)
}()
err := <-done
if err == nil {
t.Fatal("expected context deadline exceeded")
}
}
// --- helper type for group tests ---
type staticAuther struct {
id string
ok bool
}
func (s *staticAuther) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
return s.id, s.ok
}
// --- whitelist tests ---
func TestWhitelistedAuthenticator_IPMatch(t *testing.T) {
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "" || !ok {
t.Fatalf("expected (\"\", true) for matched IP, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_IPNoMatch(t *testing.T) {
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "test" || !ok {
t.Fatalf("expected (\"test\", true) for non-matched IP, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_CIDRMatch(t *testing.T) {
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"10.0.0.0/8"})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.1.2.3"), Port: 9999})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "" || !ok {
t.Fatalf("expected (\"\", true) for matched CIDR, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_CIDRNoMatch(t *testing.T) {
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"10.0.0.0/8"})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 8080})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "test" || !ok {
t.Fatalf("expected (\"test\", true) for non-matched CIDR, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_EmptyPatterns(t *testing.T) {
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "test" || !ok {
t.Fatalf("expected (\"test\", true) for empty patterns, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_NoSrcAddr(t *testing.T) {
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"})
id, ok := w.Authenticate(context.Background(), "user", "pass")
if id != "test" || !ok {
t.Fatalf("expected (\"test\", true) with no src addr in context, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_NilAuther(t *testing.T) {
// When IP matches, the whitelist returns OK regardless of nil auther.
w := WhitelistedAuthenticator(nil, []string{"192.168.1.100"})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "" || !ok {
t.Fatalf("expected (\"\", true) for matched whitelist even with nil auther, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_NilAutherNoMatch(t *testing.T) {
// When IP does not match and auther is nil, it falls through to ("", false).
w := WhitelistedAuthenticator(nil, []string{"192.168.1.100"})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 54321})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "" || ok {
t.Fatalf("expected (\"\", false) for nil auther with non-matching IP, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_UnderlyingDenies(t *testing.T) {
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: false}, []string{"10.0.0.0/8"})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 8080})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "test" || ok {
t.Fatalf("expected (\"test\", false) when underlying auther denies, got (%q, %v)", id, ok)
}
}
func TestWhitelistedAuthenticator_InvalidPatterns(t *testing.T) {
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"not-an-ip", "also-not-cidr/", ""})
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321})
id, ok := w.Authenticate(ctx, "user", "pass")
if id != "test" || !ok {
t.Fatalf("expected (\"test\", true) for all-invalid patterns, got (%q, %v)", id, ok)
}
}