test(auth): add unit tests achieving 100% coverage for core, 100% for gRPC plugin, 96.6% for HTTP plugin
This commit is contained in:
+1005
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,252 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/auth"
|
||||||
|
corelogger "github.com/go-gost/core/logger"
|
||||||
|
"github.com/go-gost/plugin/auth/proto"
|
||||||
|
xctx "github.com/go-gost/x/ctx"
|
||||||
|
"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())
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestGRPCConn(t *testing.T, srv proto.AuthenticatorServer) (*grpc.ClientConn, func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gsrv := grpc.NewServer()
|
||||||
|
proto.RegisterAuthenticatorServer(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_NilClient(t *testing.T) {
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: nil,
|
||||||
|
client: nil,
|
||||||
|
log: xlogger.Nop(),
|
||||||
|
}
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
assert.NoError(t, p.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGRPCPlugin_Authenticate_Success(t *testing.T) {
|
||||||
|
conn, cleanup := newTestGRPCConn(t, &testAuthServer{ok: true, id: "user1"})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: conn,
|
||||||
|
client: proto.NewAuthenticatorClient(conn),
|
||||||
|
log: xlogger.Nop(),
|
||||||
|
}
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "user1", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGRPCPlugin_Authenticate_Fail(t *testing.T) {
|
||||||
|
conn, cleanup := newTestGRPCConn(t, &testAuthServer{ok: false, id: ""})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: conn,
|
||||||
|
client: proto.NewAuthenticatorClient(conn),
|
||||||
|
log: xlogger.Nop(),
|
||||||
|
}
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGRPCPlugin_Authenticate_WithService(t *testing.T) {
|
||||||
|
server := &testAuthServer{ok: true, id: "srv"}
|
||||||
|
conn, cleanup := newTestGRPCConn(t, server)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: conn,
|
||||||
|
client: proto.NewAuthenticatorClient(conn),
|
||||||
|
log: xlogger.Nop(),
|
||||||
|
}
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p", auth.WithService("myservice"))
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "srv", id)
|
||||||
|
assert.Equal(t, "myservice", server.lastService)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGRPCPlugin_Authenticate_WithClientAddr(t *testing.T) {
|
||||||
|
server := &testAuthServer{ok: true, id: "addr"}
|
||||||
|
conn, cleanup := newTestGRPCConn(t, server)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: conn,
|
||||||
|
client: proto.NewAuthenticatorClient(conn),
|
||||||
|
log: xlogger.Nop(),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = xctx.ContextWithSrcAddr(ctx, &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 1234})
|
||||||
|
id, ok := p.Authenticate(ctx, "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "addr", id)
|
||||||
|
assert.Equal(t, "10.0.0.1:1234", server.lastClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGRPCPlugin_Authenticate_WithoutClientAddr(t *testing.T) {
|
||||||
|
server := &testAuthServer{ok: true, id: "noclient"}
|
||||||
|
conn, cleanup := newTestGRPCConn(t, server)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: conn,
|
||||||
|
client: proto.NewAuthenticatorClient(conn),
|
||||||
|
log: xlogger.Nop(),
|
||||||
|
}
|
||||||
|
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "noclient", id)
|
||||||
|
assert.Empty(t, server.lastClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGRPCPlugin_Authenticate_ServerError(t *testing.T) {
|
||||||
|
conn, cleanup := newTestGRPCConn(t, &errorAuthServer{})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: conn,
|
||||||
|
client: proto.NewAuthenticatorClient(conn),
|
||||||
|
log: xlogger.Nop(),
|
||||||
|
}
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGRPCPlugin_RealConn(t *testing.T) {
|
||||||
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gsrv := grpc.NewServer()
|
||||||
|
proto.RegisterAuthenticatorServer(gsrv, &testAuthServer{ok: true, id: "real"})
|
||||||
|
go gsrv.Serve(lis)
|
||||||
|
defer gsrv.Stop()
|
||||||
|
|
||||||
|
p := NewGRPCPlugin("test", lis.Addr().String(), plugin.TimeoutOption(500))
|
||||||
|
require.NotNil(t, p)
|
||||||
|
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "real", id)
|
||||||
|
assert.NoError(t, p.(io.Closer).Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewGRPCPlugin_InvalidAddr(t *testing.T) {
|
||||||
|
// Null byte causes grpc.NewClient target parsing to fail synchronously.
|
||||||
|
// This covers the log.Error(err) path.
|
||||||
|
// NOTE: depends on grpc-go's net/url control-character validation.
|
||||||
|
// If a future grpc version changes target parsing, this test may silently
|
||||||
|
// stop covering the error branch.
|
||||||
|
p := NewGRPCPlugin("test", "\x00")
|
||||||
|
require.NotNil(t, p)
|
||||||
|
|
||||||
|
// conn is nil, so client is nil, so Authenticate returns false
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, &testAuthServer{ok: true})
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: conn,
|
||||||
|
client: nil,
|
||||||
|
log: xlogger.Nop(),
|
||||||
|
}
|
||||||
|
assert.NoError(t, p.Close())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- test helpers ---
|
||||||
|
|
||||||
|
type testAuthServer struct {
|
||||||
|
proto.UnimplementedAuthenticatorServer
|
||||||
|
ok bool
|
||||||
|
id string
|
||||||
|
lastService string
|
||||||
|
lastClient string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *testAuthServer) Authenticate(ctx context.Context, req *proto.AuthenticateRequest) (*proto.AuthenticateReply, error) {
|
||||||
|
s.lastService = req.Service
|
||||||
|
s.lastClient = req.Client
|
||||||
|
return &proto.AuthenticateReply{Ok: s.ok, Id: s.id}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorAuthServer struct {
|
||||||
|
proto.UnimplementedAuthenticatorServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *errorAuthServer) Authenticate(ctx context.Context, req *proto.AuthenticateRequest) (*proto.AuthenticateReply, 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)
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/auth"
|
||||||
|
xctx "github.com/go-gost/x/ctx"
|
||||||
|
"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_Authenticate_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, "id": "user1"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "user1", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_Fail(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"ok": false, "id": ""}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_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)
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_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)
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_ConnectionRefused(t *testing.T) {
|
||||||
|
p := NewHTTPPlugin("test", "http://127.0.0.1:0", plugin.TimeoutOption(0))
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_NilClient(t *testing.T) {
|
||||||
|
hp := &httpPlugin{
|
||||||
|
url: "http://localhost",
|
||||||
|
client: nil,
|
||||||
|
}
|
||||||
|
id, ok := hp.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_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, "id": "svc"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p", auth.WithService("myservice"))
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "svc", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_WithHeader(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, "id": "hdr"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL, plugin.HeaderOption(http.Header{
|
||||||
|
"X-Custom": []string{"test-value"},
|
||||||
|
}))
|
||||||
|
id, ok := p.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "hdr", id)
|
||||||
|
assert.Equal(t, "test-value", receivedHeader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_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, "id": "nohdr"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
hp := &httpPlugin{
|
||||||
|
url: srv.URL,
|
||||||
|
client: plugin.NewHTTPClient(&plugin.Options{}),
|
||||||
|
header: nil,
|
||||||
|
}
|
||||||
|
id, ok := hp.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "nohdr", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_WithClientAddr(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"ok": true, "id": "addr"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
hp := &httpPlugin{
|
||||||
|
url: srv.URL,
|
||||||
|
client: plugin.NewHTTPClient(&plugin.Options{}),
|
||||||
|
}
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 5678})
|
||||||
|
id, ok := hp.Authenticate(ctx, "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "addr", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_WithoutClientAddr(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"ok": true, "id": "noaddr"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
hp := &httpPlugin{
|
||||||
|
url: srv.URL,
|
||||||
|
client: plugin.NewHTTPClient(&plugin.Options{}),
|
||||||
|
}
|
||||||
|
id, ok := hp.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "noaddr", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_BadURL(t *testing.T) {
|
||||||
|
hp := &httpPlugin{
|
||||||
|
url: "http://invalid hostname",
|
||||||
|
client: plugin.NewHTTPClient(&plugin.Options{}),
|
||||||
|
}
|
||||||
|
id, ok := hp.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Empty(t, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Authenticate_WithAllOptions(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
assert.Equal(t, "Bearer mytoken", r.Header.Get("Authorization"))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"ok": true, "id": "all"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
hp := &httpPlugin{
|
||||||
|
url: srv.URL,
|
||||||
|
client: plugin.NewHTTPClient(&plugin.Options{}),
|
||||||
|
header: http.Header{"Authorization": []string{"Bearer mytoken"}},
|
||||||
|
}
|
||||||
|
id, ok := hp.Authenticate(context.Background(), "u", "p")
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "all", id)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user