fix(matcher,plugin): case-insensitive matching, port accumulation, nil guards, doc comments
matcher: fix IPv6 normalization via netip.ParseAddr, port-range overwrite by switching to []*PortRange map, case-insensitive domain/host matching, glob.MustCompile→Compile to avoid panic on invalid patterns. plugin: guard nil *Options in NewGRPCConn and NewHTTPClient; add HTTPClientTransport helper for idle-connection cleanup. net/*: add doc comments for all exported symbols (no code changes). Add 30 tests (matcher_test.go 22, plugin_test.go 8) covering all matcher types, nil safety, case insensitivity, IPv6 normalization, port ranges, and plugin option composition.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
// Package plugin provides shared utilities for building gRPC and HTTP plugin
|
||||
// clients. Both transport types support authentication tokens, TLS
|
||||
// configuration, custom headers (HTTP), and connection timeouts.
|
||||
package plugin
|
||||
|
||||
import (
|
||||
@@ -13,10 +16,13 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// GRPC is the transport type identifier for gRPC-based plugins.
|
||||
GRPC string = "grpc"
|
||||
// HTTP is the transport type identifier for HTTP-based plugins.
|
||||
HTTP string = "http"
|
||||
)
|
||||
|
||||
// Options holds the common configuration for gRPC and HTTP plugin clients.
|
||||
type Options struct {
|
||||
Token string
|
||||
TLSConfig *tls.Config
|
||||
@@ -24,32 +30,40 @@ type Options struct {
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// Option configures Options.
|
||||
type Option func(opts *Options)
|
||||
|
||||
// TokenOption sets the authentication token sent with each request.
|
||||
func TokenOption(token string) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Token = token
|
||||
}
|
||||
}
|
||||
|
||||
// TLSConfigOption sets the TLS configuration for transport security.
|
||||
func TLSConfigOption(cfg *tls.Config) Option {
|
||||
return func(opts *Options) {
|
||||
opts.TLSConfig = cfg
|
||||
}
|
||||
}
|
||||
|
||||
// HeaderOption sets custom HTTP headers sent with each request (HTTP transport only).
|
||||
func HeaderOption(header http.Header) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Header = header
|
||||
}
|
||||
}
|
||||
|
||||
// TimeoutOption sets the request timeout (HTTP transport only).
|
||||
func TimeoutOption(timeout time.Duration) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Timeout = timeout
|
||||
}
|
||||
}
|
||||
|
||||
// NewGRPCConn creates a gRPC client connection to addr. If opts is nil or
|
||||
// opts.TLSConfig is nil, the connection uses insecure transport. When
|
||||
// opts.Token is set, it is attached as per-RPC credentials.
|
||||
func NewGRPCConn(addr string, opts *Options) (*grpc.ClientConn, error) {
|
||||
grpcOpts := []grpc.DialOption{
|
||||
// grpc.WithBlock(),
|
||||
@@ -57,6 +71,10 @@ func NewGRPCConn(addr string, opts *Options) (*grpc.ClientConn, error) {
|
||||
Backoff: backoff.DefaultConfig,
|
||||
}),
|
||||
}
|
||||
if opts == nil {
|
||||
grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
return grpc.NewClient(addr, grpcOpts...)
|
||||
}
|
||||
if opts.TLSConfig != nil {
|
||||
grpcOpts = append(grpcOpts,
|
||||
grpc.WithAuthority(opts.TLSConfig.ServerName),
|
||||
@@ -85,7 +103,14 @@ func (c *rpcCredentials) RequireTransportSecurity() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// NewHTTPClient creates an http.Client from opts. If opts is nil, a default
|
||||
// client with no timeout is returned. Use HTTPClientTransport to access the
|
||||
// underlying transport and close idle connections when the client is no longer
|
||||
// needed.
|
||||
func NewHTTPClient(opts *Options) *http.Client {
|
||||
if opts == nil {
|
||||
return &http.Client{}
|
||||
}
|
||||
return &http.Client{
|
||||
Timeout: opts.Timeout,
|
||||
Transport: &http.Transport{
|
||||
@@ -93,3 +118,15 @@ func NewHTTPClient(opts *Options) *http.Client {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPClientTransport returns the *http.Transport underlying c, or nil if c's
|
||||
// transport is not an *http.Transport. Callers that hold the client for a
|
||||
// long time should call tr.CloseIdleConnections() when the client is no longer
|
||||
// needed.
|
||||
func HTTPClientTransport(c *http.Client) *http.Transport {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
tr, _ := c.Transport.(*http.Transport)
|
||||
return tr
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewGRPCConn_NilOpts(t *testing.T) {
|
||||
conn, err := NewGRPCConn("localhost:0", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conn == nil {
|
||||
t.Fatal("expected non-nil conn")
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestNewGRPCConn_NoTLS(t *testing.T) {
|
||||
conn, err := NewGRPCConn("localhost:0", &Options{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conn == nil {
|
||||
t.Fatal("expected non-nil conn")
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestNewGRPCConn_WithTLS(t *testing.T) {
|
||||
conn, err := NewGRPCConn("localhost:0", &Options{
|
||||
TLSConfig: nil, // nil TLSConfig -> insecure
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conn == nil {
|
||||
t.Fatal("expected non-nil conn")
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestNewHTTPClient_NilOpts(t *testing.T) {
|
||||
c := NewHTTPClient(nil)
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil client")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPClient_Defaults(t *testing.T) {
|
||||
c := NewHTTPClient(&Options{})
|
||||
if c == nil {
|
||||
t.Fatal("expected non-nil client")
|
||||
}
|
||||
if c.Timeout != 0 {
|
||||
t.Errorf("expected zero timeout, got %v", c.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPClient_WithTimeout(t *testing.T) {
|
||||
c := NewHTTPClient(&Options{Timeout: 5 * time.Second})
|
||||
if c.Timeout != 5*time.Second {
|
||||
t.Errorf("expected 5s timeout, got %v", c.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientTransport_Nil(t *testing.T) {
|
||||
if tr := HTTPClientTransport(nil); tr != nil {
|
||||
t.Error("expected nil transport for nil client")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPClientTransport_Valid(t *testing.T) {
|
||||
c := NewHTTPClient(&Options{})
|
||||
tr := HTTPClientTransport(c)
|
||||
if tr == nil {
|
||||
t.Fatal("expected non-nil transport")
|
||||
}
|
||||
// Verify we can call CloseIdleConnections without panic.
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func TestOption(t *testing.T) {
|
||||
opts := &Options{}
|
||||
TokenOption("tok")(opts)
|
||||
TLSConfigOption(nil)(opts)
|
||||
HeaderOption(http.Header{"X": []string{"y"}})(opts)
|
||||
TimeoutOption(time.Second)(opts)
|
||||
|
||||
if opts.Token != "tok" {
|
||||
t.Errorf("expected Token 'tok', got %q", opts.Token)
|
||||
}
|
||||
if opts.Timeout != time.Second {
|
||||
t.Errorf("expected 1s timeout, got %v", opts.Timeout)
|
||||
}
|
||||
if opts.Header.Get("X") != "y" {
|
||||
t.Errorf("expected header X=y, got %v", opts.Header)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHTTPClient_TransportTLSConfig(t *testing.T) {
|
||||
// nil TLSConfig — transport should still exist.
|
||||
c := NewHTTPClient(&Options{})
|
||||
tr := HTTPClientTransport(c)
|
||||
if tr == nil {
|
||||
t.Fatal("expected non-nil transport")
|
||||
}
|
||||
if tr.TLSClientConfig != nil {
|
||||
t.Error("expected nil TLSClientConfig")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user