fix(sd): add nil service guards, improve error messages, remove unused logger
- Add nil service guards to Register/Deregister/Renew in gRPC and HTTP plugins to prevent nil pointer dereferences - Remove unused log field from grpcPlugin and httpPlugin - Prefix HTTP error messages with operation context (sd register/deregister/renew/get) - Return gRPC Register error directly instead of logging + returning - Add 33 unit tests covering all operations, nil guards, and error paths
This commit is contained in:
+6
-15
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/go-gost/core/logger"
|
|
||||||
"github.com/go-gost/core/sd"
|
"github.com/go-gost/core/sd"
|
||||||
"github.com/go-gost/plugin/sd/proto"
|
"github.com/go-gost/plugin/sd/proto"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
@@ -14,7 +13,6 @@ import (
|
|||||||
type grpcPlugin struct {
|
type grpcPlugin struct {
|
||||||
conn grpc.ClientConnInterface
|
conn grpc.ClientConnInterface
|
||||||
client proto.SDClient
|
client proto.SDClient
|
||||||
log logger.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGRPCPlugin creates an SD plugin based on gRPC.
|
// NewGRPCPlugin creates an SD plugin based on gRPC.
|
||||||
@@ -24,18 +22,15 @@ func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) sd.SD {
|
|||||||
opt(&options)
|
opt(&options)
|
||||||
}
|
}
|
||||||
|
|
||||||
log := logger.Default().WithFields(map[string]any{
|
|
||||||
"kind": "sd",
|
|
||||||
"sd": name,
|
|
||||||
})
|
|
||||||
conn, err := plugin.NewGRPCConn(addr, &options)
|
conn, err := plugin.NewGRPCConn(addr, &options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
// Connection error is logged by the gRPC dialer; the plugin
|
||||||
|
// will silently no-op all operations until the connection is
|
||||||
|
// established (client stays nil).
|
||||||
}
|
}
|
||||||
|
|
||||||
p := &grpcPlugin{
|
p := &grpcPlugin{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
log: log,
|
|
||||||
}
|
}
|
||||||
if conn != nil {
|
if conn != nil {
|
||||||
p.client = proto.NewSDClient(conn)
|
p.client = proto.NewSDClient(conn)
|
||||||
@@ -44,7 +39,7 @@ func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) sd.SD {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *grpcPlugin) Register(ctx context.Context, service *sd.Service, opts ...sd.Option) error {
|
func (p *grpcPlugin) Register(ctx context.Context, service *sd.Service, opts ...sd.Option) error {
|
||||||
if p.client == nil {
|
if p.client == nil || service == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,15 +53,11 @@ func (p *grpcPlugin) Register(ctx context.Context, service *sd.Service, opts ...
|
|||||||
Address: service.Address,
|
Address: service.Address,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
p.log.Error(err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *grpcPlugin) Deregister(ctx context.Context, service *sd.Service) error {
|
func (p *grpcPlugin) Deregister(ctx context.Context, service *sd.Service) error {
|
||||||
if p.client == nil {
|
if p.client == nil || service == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +74,7 @@ func (p *grpcPlugin) Deregister(ctx context.Context, service *sd.Service) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *grpcPlugin) Renew(ctx context.Context, service *sd.Service) error {
|
func (p *grpcPlugin) Renew(ctx context.Context, service *sd.Service) error {
|
||||||
if p.client == nil {
|
if p.client == nil || service == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
package sd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/sd"
|
||||||
|
"github.com/go-gost/plugin/sd/proto"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mockSDClient implements proto.SDClient for testing.
|
||||||
|
type mockSDClient struct {
|
||||||
|
registerErr error
|
||||||
|
deregisterErr error
|
||||||
|
renewErr error
|
||||||
|
getReply *proto.GetServiceReply
|
||||||
|
getErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSDClient) Register(ctx context.Context, in *proto.RegisterRequest, opts ...grpc.CallOption) (*proto.RegisterReply, error) {
|
||||||
|
if m.registerErr != nil {
|
||||||
|
return nil, m.registerErr
|
||||||
|
}
|
||||||
|
return &proto.RegisterReply{Ok: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSDClient) Deregister(ctx context.Context, in *proto.DeregisterRequest, opts ...grpc.CallOption) (*proto.DeregisterReply, error) {
|
||||||
|
if m.deregisterErr != nil {
|
||||||
|
return nil, m.deregisterErr
|
||||||
|
}
|
||||||
|
return &proto.DeregisterReply{Ok: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSDClient) Renew(ctx context.Context, in *proto.RenewRequest, opts ...grpc.CallOption) (*proto.RenewReply, error) {
|
||||||
|
if m.renewErr != nil {
|
||||||
|
return nil, m.renewErr
|
||||||
|
}
|
||||||
|
return &proto.RenewReply{Ok: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSDClient) Get(ctx context.Context, in *proto.GetServiceRequest, opts ...grpc.CallOption) (*proto.GetServiceReply, error) {
|
||||||
|
if m.getErr != nil {
|
||||||
|
return nil, m.getErr
|
||||||
|
}
|
||||||
|
return m.getReply, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mockGrpcConn implements grpc.ClientConnInterface and io.Closer.
|
||||||
|
type mockGrpcConn struct {
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockGrpcConn) Invoke(ctx context.Context, method string, args, reply any, opts ...grpc.CallOption) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockGrpcConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockGrpcConn) Close() error {
|
||||||
|
m.closed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- NewGRPCPlugin ---
|
||||||
|
|
||||||
|
func TestNewGRPCPlugin_ReturnsNonNil(t *testing.T) {
|
||||||
|
p := NewGRPCPlugin("test", "127.0.0.1:65535")
|
||||||
|
if p == nil {
|
||||||
|
t.Fatal("NewGRPCPlugin should never return nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Register ---
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Register_NilClient(t *testing.T) {
|
||||||
|
p := &grpcPlugin{}
|
||||||
|
if err := p.Register(context.Background(), &sd.Service{ID: "1"}); err != nil {
|
||||||
|
t.Errorf("nil client Register should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Register_NilService(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{}}
|
||||||
|
if err := p.Register(context.Background(), nil); err != nil {
|
||||||
|
t.Errorf("nil service Register should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Register_Success(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{}}
|
||||||
|
err := p.Register(context.Background(), &sd.Service{
|
||||||
|
ID: "svc-1", Name: "test", Node: "n1", Network: "tcp", Address: "127.0.0.1:8080",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Register failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Register_ServerError(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{registerErr: io.ErrUnexpectedEOF}}
|
||||||
|
err := p.Register(context.Background(), &sd.Service{ID: "1"})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Register should return error when server fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Deregister ---
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Deregister_NilClient(t *testing.T) {
|
||||||
|
p := &grpcPlugin{}
|
||||||
|
if err := p.Deregister(context.Background(), &sd.Service{ID: "1"}); err != nil {
|
||||||
|
t.Errorf("nil client Deregister should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Deregister_NilService(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{}}
|
||||||
|
if err := p.Deregister(context.Background(), nil); err != nil {
|
||||||
|
t.Errorf("nil service Deregister should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Deregister_Success(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{}}
|
||||||
|
err := p.Deregister(context.Background(), &sd.Service{
|
||||||
|
ID: "svc-1", Name: "test", Node: "n1", Network: "tcp", Address: "127.0.0.1:8080",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Deregister failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Deregister_ServerError(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{deregisterErr: io.ErrUnexpectedEOF}}
|
||||||
|
err := p.Deregister(context.Background(), &sd.Service{ID: "1"})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Deregister should return error when server fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Renew ---
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Renew_NilClient(t *testing.T) {
|
||||||
|
p := &grpcPlugin{}
|
||||||
|
if err := p.Renew(context.Background(), &sd.Service{ID: "1"}); err != nil {
|
||||||
|
t.Errorf("nil client Renew should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Renew_NilService(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{}}
|
||||||
|
if err := p.Renew(context.Background(), nil); err != nil {
|
||||||
|
t.Errorf("nil service Renew should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Renew_Success(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{}}
|
||||||
|
err := p.Renew(context.Background(), &sd.Service{
|
||||||
|
ID: "svc-1", Name: "test", Node: "n1", Network: "tcp", Address: "127.0.0.1:8080",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Renew failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Renew_ServerError(t *testing.T) {
|
||||||
|
p := &grpcPlugin{client: &mockSDClient{renewErr: io.ErrUnexpectedEOF}}
|
||||||
|
err := p.Renew(context.Background(), &sd.Service{ID: "1"})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Renew should return error when server fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Get ---
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Get_NilClient(t *testing.T) {
|
||||||
|
p := &grpcPlugin{}
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("nil client Get should return nil error, got %v", err)
|
||||||
|
}
|
||||||
|
if services != nil {
|
||||||
|
t.Errorf("nil client Get should return nil services, got %v", services)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Get_Success(t *testing.T) {
|
||||||
|
p := &grpcPlugin{
|
||||||
|
client: &mockSDClient{
|
||||||
|
getReply: &proto.GetServiceReply{
|
||||||
|
Services: []*proto.Service{
|
||||||
|
{Id: "1", Name: "test", Node: "n1", Network: "tcp", Address: "10.0.0.1:80"},
|
||||||
|
{Id: "2", Name: "test", Node: "n2", Network: "udp", Address: "10.0.0.2:53"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(services) != 2 {
|
||||||
|
t.Fatalf("expected 2 services, got %d", len(services))
|
||||||
|
}
|
||||||
|
if services[0].ID != "1" || services[0].Network != "tcp" {
|
||||||
|
t.Errorf("service[0] = %+v, want ID=1 Network=tcp", services[0])
|
||||||
|
}
|
||||||
|
if services[1].ID != "2" || services[1].Network != "udp" {
|
||||||
|
t.Errorf("service[1] = %+v, want ID=2 Network=udp", services[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Get_SkipsNilServices(t *testing.T) {
|
||||||
|
p := &grpcPlugin{
|
||||||
|
client: &mockSDClient{
|
||||||
|
getReply: &proto.GetServiceReply{
|
||||||
|
Services: []*proto.Service{
|
||||||
|
{Id: "1", Name: "test"},
|
||||||
|
nil,
|
||||||
|
{Id: "3", Name: "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(services) != 2 {
|
||||||
|
t.Fatalf("expected 2 non-nil services, got %d", len(services))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Get_EmptyServices(t *testing.T) {
|
||||||
|
p := &grpcPlugin{
|
||||||
|
client: &mockSDClient{
|
||||||
|
getReply: &proto.GetServiceReply{Services: nil},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(services) != 0 {
|
||||||
|
t.Errorf("expected 0 services, got %d", len(services))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Get_ServerError(t *testing.T) {
|
||||||
|
p := &grpcPlugin{
|
||||||
|
client: &mockSDClient{getErr: io.ErrUnexpectedEOF},
|
||||||
|
}
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Get should return error when server fails")
|
||||||
|
}
|
||||||
|
if services != nil {
|
||||||
|
t.Errorf("expected nil services on error, got %v", services)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Close ---
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Close_NilConn(t *testing.T) {
|
||||||
|
p := &grpcPlugin{conn: nil}
|
||||||
|
if err := p.Close(); err != nil {
|
||||||
|
t.Errorf("Close with nil conn should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Close_WithConn(t *testing.T) {
|
||||||
|
conn := &mockGrpcConn{}
|
||||||
|
p := &grpcPlugin{conn: conn}
|
||||||
|
if err := p.Close(); err != nil {
|
||||||
|
t.Errorf("Close failed: %v", err)
|
||||||
|
}
|
||||||
|
if !conn.closed {
|
||||||
|
t.Error("Close should close the underlying connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGrpcPlugin_Close_ConnWithoutCloser(t *testing.T) {
|
||||||
|
// A conn that implements ClientConnInterface but not io.Closer.
|
||||||
|
p := &grpcPlugin{conn: &struct{ grpc.ClientConnInterface }{}}
|
||||||
|
if err := p.Close(); err != nil {
|
||||||
|
t.Errorf("Close on non-io.Closer conn should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Interface assertion ---
|
||||||
|
|
||||||
|
var _ sd.SD = (*grpcPlugin)(nil)
|
||||||
+5
-12
@@ -4,11 +4,9 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-gost/core/logger"
|
|
||||||
"github.com/go-gost/core/sd"
|
"github.com/go-gost/core/sd"
|
||||||
"github.com/go-gost/x/internal/plugin"
|
"github.com/go-gost/x/internal/plugin"
|
||||||
)
|
)
|
||||||
@@ -29,7 +27,6 @@ type httpPlugin struct {
|
|||||||
url string
|
url string
|
||||||
client *http.Client
|
client *http.Client
|
||||||
header http.Header
|
header http.Header
|
||||||
log logger.Logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHTTPPlugin creates an SD plugin based on HTTP.
|
// NewHTTPPlugin creates an SD plugin based on HTTP.
|
||||||
@@ -43,10 +40,6 @@ func NewHTTPPlugin(name string, url string, opts ...plugin.Option) sd.SD {
|
|||||||
url: url,
|
url: url,
|
||||||
client: plugin.NewHTTPClient(&options),
|
client: plugin.NewHTTPClient(&options),
|
||||||
header: options.Header,
|
header: options.Header,
|
||||||
log: logger.Default().WithFields(map[string]any{
|
|
||||||
"kind": "sd",
|
|
||||||
"sd": name,
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +75,7 @@ func (p *httpPlugin) Register(ctx context.Context, service *sd.Service, opts ...
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return errors.New(resp.Status)
|
return fmt.Errorf("sd register: %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -120,14 +113,14 @@ func (p *httpPlugin) Deregister(ctx context.Context, service *sd.Service) error
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return errors.New(resp.Status)
|
return fmt.Errorf("sd deregister: %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *httpPlugin) Renew(ctx context.Context, service *sd.Service) error {
|
func (p *httpPlugin) Renew(ctx context.Context, service *sd.Service) error {
|
||||||
if p.client == nil {
|
if p.client == nil || service == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +151,7 @@ func (p *httpPlugin) Renew(ctx context.Context, service *sd.Service) error {
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf(resp.Status)
|
return fmt.Errorf("sd renew: %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -190,7 +183,7 @@ func (p *httpPlugin) Get(ctx context.Context, name string) (services []*sd.Servi
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf(resp.Status)
|
return nil, fmt.Errorf("sd get: %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &httpGetResponse{}
|
res := &httpGetResponse{}
|
||||||
|
|||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package sd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/sd"
|
||||||
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- NewHTTPPlugin ---
|
||||||
|
|
||||||
|
func TestNewHTTPPlugin_ReturnsNonNil(t *testing.T) {
|
||||||
|
p := NewHTTPPlugin("test", "http://localhost:8080")
|
||||||
|
if p == nil {
|
||||||
|
t.Fatal("NewHTTPPlugin should not return nil")
|
||||||
|
}
|
||||||
|
hp, ok := p.(*httpPlugin)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("NewHTTPPlugin returned %T, want *httpPlugin", p)
|
||||||
|
}
|
||||||
|
if hp.url != "http://localhost:8080" {
|
||||||
|
t.Errorf("url = %s, want http://localhost:8080", hp.url)
|
||||||
|
}
|
||||||
|
if hp.client == nil {
|
||||||
|
t.Error("client should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Register ---
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Register_NilClient(t *testing.T) {
|
||||||
|
p := &httpPlugin{}
|
||||||
|
if err := p.Register(context.Background(), &sd.Service{ID: "1"}); err != nil {
|
||||||
|
t.Errorf("nil client Register should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Register_NilService(t *testing.T) {
|
||||||
|
p := &httpPlugin{client: &http.Client{}}
|
||||||
|
if err := p.Register(context.Background(), nil); err != nil {
|
||||||
|
t.Errorf("nil service Register should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Register_Success(t *testing.T) {
|
||||||
|
var gotBody sdService
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
t.Errorf("expected POST, got %s", r.Method)
|
||||||
|
}
|
||||||
|
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
||||||
|
t.Errorf("Content-Type = %s, want application/json", ct)
|
||||||
|
}
|
||||||
|
json.NewDecoder(r.Body).Decode(&gotBody)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
svc := &sd.Service{ID: "svc-1", Name: "test", Node: "n1", Network: "tcp", Address: "10.0.0.1:80"}
|
||||||
|
if err := p.Register(context.Background(), svc); err != nil {
|
||||||
|
t.Fatalf("Register failed: %v", err)
|
||||||
|
}
|
||||||
|
if gotBody.ID != "svc-1" {
|
||||||
|
t.Errorf("body ID = %s, want svc-1", gotBody.ID)
|
||||||
|
}
|
||||||
|
if gotBody.Network != "tcp" {
|
||||||
|
t.Errorf("body Network = %s, want tcp", gotBody.Network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Register_ServerError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
err := p.Register(context.Background(), &sd.Service{ID: "1"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Register should return error on non-200 response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Deregister ---
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Deregister_NilClient(t *testing.T) {
|
||||||
|
p := &httpPlugin{}
|
||||||
|
if err := p.Deregister(context.Background(), &sd.Service{ID: "1"}); err != nil {
|
||||||
|
t.Errorf("nil client Deregister should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Deregister_NilService(t *testing.T) {
|
||||||
|
p := &httpPlugin{client: &http.Client{}}
|
||||||
|
if err := p.Deregister(context.Background(), nil); err != nil {
|
||||||
|
t.Errorf("nil service Deregister should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Deregister_Success(t *testing.T) {
|
||||||
|
var gotMethod string
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotMethod = r.Method
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
if err := p.Deregister(context.Background(), &sd.Service{ID: "svc-1"}); err != nil {
|
||||||
|
t.Fatalf("Deregister failed: %v", err)
|
||||||
|
}
|
||||||
|
if gotMethod != http.MethodDelete {
|
||||||
|
t.Errorf("method = %s, want DELETE", gotMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Deregister_ServerError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
err := p.Deregister(context.Background(), &sd.Service{ID: "1"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Deregister should return error on non-200 response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Renew ---
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Renew_NilClient(t *testing.T) {
|
||||||
|
p := &httpPlugin{}
|
||||||
|
if err := p.Renew(context.Background(), &sd.Service{ID: "1"}); err != nil {
|
||||||
|
t.Errorf("nil client Renew should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Renew_NilService(t *testing.T) {
|
||||||
|
p := &httpPlugin{client: &http.Client{}}
|
||||||
|
if err := p.Renew(context.Background(), nil); err != nil {
|
||||||
|
t.Errorf("nil service Renew should return nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Renew_Success(t *testing.T) {
|
||||||
|
var gotMethod string
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotMethod = r.Method
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
if err := p.Renew(context.Background(), &sd.Service{ID: "svc-1"}); err != nil {
|
||||||
|
t.Fatalf("Renew failed: %v", err)
|
||||||
|
}
|
||||||
|
if gotMethod != http.MethodPut {
|
||||||
|
t.Errorf("method = %s, want PUT", gotMethod)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Renew_ServerError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
err := p.Renew(context.Background(), &sd.Service{ID: "1"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Renew should return error on non-200 response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Get ---
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Get_NilClient(t *testing.T) {
|
||||||
|
p := &httpPlugin{}
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("nil client Get should return nil error, got %v", err)
|
||||||
|
}
|
||||||
|
if services != nil {
|
||||||
|
t.Errorf("nil client Get should return nil services, got %v", services)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Get_Success(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
t.Errorf("expected GET, got %s", r.Method)
|
||||||
|
}
|
||||||
|
if name := r.URL.Query().Get("name"); name != "my-svc" {
|
||||||
|
t.Errorf("name query = %s, want my-svc", name)
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"services":[
|
||||||
|
{"id":"1","name":"my-svc","node":"n1","network":"tcp","address":"10.0.0.1:80"},
|
||||||
|
{"id":"2","name":"my-svc","node":"n2","network":"udp","address":"10.0.0.2:53"}
|
||||||
|
]}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
services, err := p.Get(context.Background(), "my-svc")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(services) != 2 {
|
||||||
|
t.Fatalf("expected 2 services, got %d", len(services))
|
||||||
|
}
|
||||||
|
if services[0].ID != "1" || services[0].Network != "tcp" {
|
||||||
|
t.Errorf("service[0] = %+v, want ID=1 Network=tcp", services[0])
|
||||||
|
}
|
||||||
|
if services[1].ID != "2" || services[1].Network != "udp" {
|
||||||
|
t.Errorf("service[1] = %+v, want ID=2 Network=udp", services[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Get_SkipsNilServices(t *testing.T) {
|
||||||
|
// The JSON response has a null entry in the services array. After decoding,
|
||||||
|
// Get should skip nil entries.
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"services":[{"id":"1","name":"test"},null,{"id":"3","name":"test"}]}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(services) != 2 {
|
||||||
|
t.Fatalf("expected 2 non-nil services, got %d", len(services))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Get_EmptyServices(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"services":[]}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(services) != 0 {
|
||||||
|
t.Errorf("expected 0 services, got %d", len(services))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Get_ServerError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
services, err := p.Get(context.Background(), "test")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Get should return error on non-200 response")
|
||||||
|
}
|
||||||
|
if services != nil {
|
||||||
|
t.Errorf("expected nil services on error, got %v", services)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Get_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)
|
||||||
|
_, err := p.Get(context.Background(), "test")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Get should return error on invalid JSON response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Custom Header ---
|
||||||
|
|
||||||
|
func TestHTTPPlugin_CustomHeader(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)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL, plugin.HeaderOption(http.Header{
|
||||||
|
"X-Custom": []string{"my-value"},
|
||||||
|
}))
|
||||||
|
err := p.Register(context.Background(), &sd.Service{ID: "1", Name: "test"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if receivedHeader != "my-value" {
|
||||||
|
t.Errorf("X-Custom header = %s, want my-value", receivedHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Context cancellation ---
|
||||||
|
|
||||||
|
func TestHTTPPlugin_Register_CanceledContext(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
p := NewHTTPPlugin("test", srv.URL)
|
||||||
|
err := p.Register(ctx, &sd.Service{ID: "1"})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Register should return error with canceled context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Interface assertion ---
|
||||||
|
|
||||||
|
var _ sd.SD = (*httpPlugin)(nil)
|
||||||
Reference in New Issue
Block a user