diff --git a/sd/plugin/grpc.go b/sd/plugin/grpc.go index b8fe5a4b..5c7567ba 100644 --- a/sd/plugin/grpc.go +++ b/sd/plugin/grpc.go @@ -4,7 +4,6 @@ import ( "context" "io" - "github.com/go-gost/core/logger" "github.com/go-gost/core/sd" "github.com/go-gost/plugin/sd/proto" "github.com/go-gost/x/internal/plugin" @@ -14,7 +13,6 @@ import ( type grpcPlugin struct { conn grpc.ClientConnInterface client proto.SDClient - log logger.Logger } // 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) } - log := logger.Default().WithFields(map[string]any{ - "kind": "sd", - "sd": name, - }) conn, err := plugin.NewGRPCConn(addr, &options) 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{ conn: conn, - log: log, } if conn != nil { 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 { - if p.client == nil { + if p.client == nil || service == nil { return nil } @@ -58,15 +53,11 @@ func (p *grpcPlugin) Register(ctx context.Context, service *sd.Service, opts ... Address: service.Address, }, }) - if err != nil { - p.log.Error(err) - return err - } - return nil + return err } func (p *grpcPlugin) Deregister(ctx context.Context, service *sd.Service) error { - if p.client == nil { + if p.client == nil || service == 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 { - if p.client == nil { + if p.client == nil || service == nil { return nil } diff --git a/sd/plugin/grpc_test.go b/sd/plugin/grpc_test.go new file mode 100644 index 00000000..a94cb0ba --- /dev/null +++ b/sd/plugin/grpc_test.go @@ -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) diff --git a/sd/plugin/http.go b/sd/plugin/http.go index 1a38ece8..e46949d7 100644 --- a/sd/plugin/http.go +++ b/sd/plugin/http.go @@ -4,11 +4,9 @@ import ( "bytes" "context" "encoding/json" - "errors" "fmt" "net/http" - "github.com/go-gost/core/logger" "github.com/go-gost/core/sd" "github.com/go-gost/x/internal/plugin" ) @@ -29,7 +27,6 @@ type httpPlugin struct { url string client *http.Client header http.Header - log logger.Logger } // 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, client: plugin.NewHTTPClient(&options), 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() if resp.StatusCode != http.StatusOK { - return errors.New(resp.Status) + return fmt.Errorf("sd register: %s", resp.Status) } return nil @@ -120,14 +113,14 @@ func (p *httpPlugin) Deregister(ctx context.Context, service *sd.Service) error defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return errors.New(resp.Status) + return fmt.Errorf("sd deregister: %s", resp.Status) } return nil } func (p *httpPlugin) Renew(ctx context.Context, service *sd.Service) error { - if p.client == nil { + if p.client == nil || service == nil { return nil } @@ -158,7 +151,7 @@ func (p *httpPlugin) Renew(ctx context.Context, service *sd.Service) error { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf(resp.Status) + return fmt.Errorf("sd renew: %s", resp.Status) } return nil @@ -190,7 +183,7 @@ func (p *httpPlugin) Get(ctx context.Context, name string) (services []*sd.Servi defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf(resp.Status) + return nil, fmt.Errorf("sd get: %s", resp.Status) } res := &httpGetResponse{} diff --git a/sd/plugin/http_test.go b/sd/plugin/http_test.go new file mode 100644 index 00000000..f8988297 --- /dev/null +++ b/sd/plugin/http_test.go @@ -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)