From 6d61fcd7e703feb65b3ae0509647a7d9e3de87f6 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 15 Apr 2026 20:56:22 -0400 Subject: [PATCH] Wrote pool config and tests. --- config.go | 169 +++++++++++++++-- config_test.go => config_connection_test.go | 190 ++++++++++---------- config_pool_test.go | 145 +++++++++++++++ errors/errors.go | 4 +- 4 files changed, 401 insertions(+), 107 deletions(-) rename config_test.go => config_connection_test.go (52%) create mode 100644 config_pool_test.go diff --git a/config.go b/config.go index 7e5e975..bc5de23 100644 --- a/config.go +++ b/config.go @@ -13,6 +13,82 @@ type CloseHandler func(code int, text string) error type PoolConfig struct { ConnectionConfig *ConnectionConfig + IdleTimeout time.Duration +} + +type PoolOption func(*PoolConfig) error + +func NewPoolConfig(options ...PoolOption) (*PoolConfig, error) { + conf := GetDefaultPoolConfig() + if err := applyPoolOptions(conf, options...); err != nil { + return nil, err + } + if err := validatePoolConfig(conf); err != nil { + return nil, err + } + return conf, nil +} + +func GetDefaultPoolConfig() *PoolConfig { + return &PoolConfig{ + IdleTimeout: 20 * time.Second, + ConnectionConfig: nil, + } +} + +func applyPoolOptions(config *PoolConfig, options ...PoolOption) error { + for _, option := range options { + if err := option(config); err != nil { + return err + } + } + return nil +} + +func validatePoolConfig(config *PoolConfig) error { + err := validateIdleTimeout(config.IdleTimeout) + if err != nil { + return err + } + + if config.ConnectionConfig != nil { + err = validateConnectionConfig(config.ConnectionConfig) + if err != nil { + return err + } + } + + return nil +} + +func validateIdleTimeout(value time.Duration) error { + if value < 0 { + return errors.InvalidIdleTimeout + } + return nil +} + +// When IdleTimeout is set to zero, idle timeouts are disabled. +func WithIdleTimeout(value time.Duration) PoolOption { + return func(c *PoolConfig) error { + err := validateIdleTimeout(value) + if err != nil { + return err + } + c.IdleTimeout = value + return nil + } +} + +func WithConnectionConfig(cc *ConnectionConfig) PoolOption { + return func(c *PoolConfig) error { + err := validateConnectionConfig(cc) + if err != nil { + return err + } + c.ConnectionConfig = cc + return nil + } } // Connection Config @@ -70,7 +146,32 @@ func applyConnectionOptions(config *ConnectionConfig, options ...ConnectionOptio } func validateConnectionConfig(config *ConnectionConfig) error { + err := validateWriteTimeout(config.WriteTimeout) + if err != nil { + return err + } + if config.Retry != nil { + err = validateMaxRetries(config.Retry.MaxRetries) + if err != nil { + return err + } + + err = validateInitialDelay(config.Retry.InitialDelay) + if err != nil { + return err + } + + err = validateMaxDelay(config.Retry.MaxDelay) + if err != nil { + return err + } + + err = validateJitterFactor(config.Retry.JitterFactor) + if err != nil { + return err + } + if config.Retry.InitialDelay > config.Retry.MaxDelay { return errors.NewConfigError("initial delay may not exceed maximum delay") } @@ -79,7 +180,40 @@ func validateConnectionConfig(config *ConnectionConfig) error { return nil } -// Configuration Options +func validateWriteTimeout(value time.Duration) error { + if value < 0 { + return errors.InvalidWriteTimeout + } + return nil +} + +func validateMaxRetries(value int) error { + if value < 0 { + return errors.InvalidRetryMaxRetries + } + return nil +} + +func validateInitialDelay(value time.Duration) error { + if value <= 0 { + return errors.InvalidRetryInitialDelay + } + return nil +} + +func validateMaxDelay(value time.Duration) error { + if value <= 0 { + return errors.InvalidRetryMaxDelay + } + return nil +} + +func validateJitterFactor(value float64) error { + if value < 0.0 || value > 1.0 { + return errors.InvalidRetryJitterFactor + } + return nil +} func WithCloseHandler(handler CloseHandler) ConnectionOption { return func(c *ConnectionConfig) error { @@ -91,8 +225,9 @@ func WithCloseHandler(handler CloseHandler) ConnectionOption { // When WriteTimeout is set to zero, read timeouts are disabled. func WithWriteTimeout(value time.Duration) ConnectionOption { return func(c *ConnectionConfig) error { - if value < 0 { - return errors.InvalidWriteTimeout + err := validateWriteTimeout(value) + if err != nil { + return err } c.WriteTimeout = value return nil @@ -117,9 +252,12 @@ func WithRetryMaxRetries(value int) ConnectionOption { if c.Retry == nil { c.Retry = GetDefaultRetryConfig() } - if value < 0 { - return errors.InvalidRetryMaxRetries + + err := validateMaxRetries(value) + if err != nil { + return err } + c.Retry.MaxRetries = value return nil } @@ -130,9 +268,12 @@ func WithRetryInitialDelay(value time.Duration) ConnectionOption { if c.Retry == nil { c.Retry = GetDefaultRetryConfig() } - if value <= 0 { - return errors.InvalidRetryInitialDelay + + err := validateInitialDelay(value) + if err != nil { + return err } + c.Retry.InitialDelay = value return nil } @@ -143,9 +284,12 @@ func WithRetryMaxDelay(value time.Duration) ConnectionOption { if c.Retry == nil { c.Retry = GetDefaultRetryConfig() } - if value <= 0 { - return errors.InvalidRetryMaxDelay + + err := validateMaxDelay(value) + if err != nil { + return err } + c.Retry.MaxDelay = value return nil } @@ -156,9 +300,12 @@ func WithRetryJitterFactor(value float64) ConnectionOption { if c.Retry == nil { c.Retry = GetDefaultRetryConfig() } - if value < 0.0 || value > 1.0 { - return errors.InvalidRetryJitterFactor + + err := validateJitterFactor(value) + if err != nil { + return err } + c.Retry.JitterFactor = value return nil } diff --git a/config_test.go b/config_connection_test.go similarity index 52% rename from config_test.go rename to config_connection_test.go index 1a5670b..9d38c83 100644 --- a/config_test.go +++ b/config_connection_test.go @@ -7,9 +7,9 @@ import ( "time" ) -// Config Tests +// Connection Config Tests -func TestNewConfig(t *testing.T) { +func TestNewConnectionConfig(t *testing.T) { conf, err := NewConnectionConfig() assert.NoError(t, err) @@ -27,9 +27,9 @@ func TestNewConfig(t *testing.T) { assert.Error(t, err) } -// Default Config Tests +// Default Tests -func TestDefaultConfig(t *testing.T) { +func TestDefaultConnectionConfig(t *testing.T) { conf := GetDefaultConnectionConfig() assert.Equal(t, conf, &ConnectionConfig{ @@ -39,7 +39,7 @@ func TestDefaultConfig(t *testing.T) { }) } -func TestDefaultRetryConfig(t *testing.T) { +func TestDefaultRetryConnectionConfig(t *testing.T) { conf := GetDefaultRetryConfig() assert.Equal(t, conf, &RetryConfig{ @@ -50,9 +50,9 @@ func TestDefaultRetryConfig(t *testing.T) { }) } -// Config Builder Tests +// Builder Tests -func TestSetConfig(t *testing.T) { +func TestApplyConnectionOptions(t *testing.T) { conf := &ConnectionConfig{} err := applyConnectionOptions( conf, @@ -75,7 +75,7 @@ func TestSetConfig(t *testing.T) { assert.ErrorIs(t, err, errors.InvalidRetryMaxRetries) } -// Config Option Tests +// Option Tests func TestWithCloseHandler(t *testing.T) { conf := &ConnectionConfig{} @@ -104,98 +104,100 @@ func TestWithWriteTimeout(t *testing.T) { opt = WithWriteTimeout(-30) err = applyConnectionOptions(conf, opt) assert.ErrorIs(t, err, errors.InvalidWriteTimeout) - assert.ErrorContains(t, err, "write timeout must be positive") + assert.ErrorContains(t, err, "write timeout cannot be negative") } func TestWithRetry(t *testing.T) { - conf := &ConnectionConfig{} - opt := WithRetry() - err := applyConnectionOptions(conf, opt) - assert.NoError(t, err) - assert.NotNil(t, conf.Retry) - assert.Equal(t, conf.Retry, GetDefaultRetryConfig()) + t.Run("default", func(t *testing.T) { + conf := &ConnectionConfig{} + opt := WithRetry() + err := applyConnectionOptions(conf, opt) + assert.NoError(t, err) + assert.NotNil(t, conf.Retry) + assert.Equal(t, conf.Retry, GetDefaultRetryConfig()) + }) + + t.Run("with attempts", func(t *testing.T) { + conf := &ConnectionConfig{} + opt := WithRetryMaxRetries(3) + err := applyConnectionOptions(conf, opt) + assert.NoError(t, err) + assert.Equal(t, 3, conf.Retry.MaxRetries) + + // zero allowed + opt = WithRetryMaxRetries(0) + err = applyConnectionOptions(conf, opt) + assert.NoError(t, err) + + // negative disallowed + opt = WithRetryMaxRetries(-10) + err = applyConnectionOptions(conf, opt) + assert.ErrorIs(t, err, errors.InvalidRetryMaxRetries) + assert.ErrorContains(t, err, "max retry count cannot be negative") + }) + + t.Run("with initial delay", func(t *testing.T) { + conf := &ConnectionConfig{} + opt := WithRetryInitialDelay(10 * time.Second) + err := applyConnectionOptions(conf, opt) + assert.NoError(t, err) + assert.Equal(t, 10*time.Second, conf.Retry.InitialDelay) + + // zero disallowed + opt = WithRetryInitialDelay(0 * time.Second) + err = applyConnectionOptions(conf, opt) + assert.ErrorIs(t, err, errors.InvalidRetryInitialDelay) + assert.ErrorContains(t, err, "initial delay must be positive") + + // negative disallowed + opt = WithRetryInitialDelay(-10 * time.Second) + err = applyConnectionOptions(conf, opt) + assert.ErrorIs(t, err, errors.InvalidRetryInitialDelay) + }) + + t.Run("with max delay", func(t *testing.T) { + conf := &ConnectionConfig{} + opt := WithRetryMaxDelay(10 * time.Second) + err := applyConnectionOptions(conf, opt) + assert.NoError(t, err) + assert.Equal(t, 10*time.Second, conf.Retry.MaxDelay) + + // zero disallowed + opt = WithRetryMaxDelay(0 * time.Second) + err = applyConnectionOptions(conf, opt) + assert.ErrorIs(t, err, errors.InvalidRetryMaxDelay) + assert.ErrorContains(t, err, "max delay must be positive") + + // negative disallowed + opt = WithRetryMaxDelay(-10 * time.Second) + err = applyConnectionOptions(conf, opt) + assert.ErrorIs(t, err, errors.InvalidRetryMaxDelay) + }) + + t.Run("with jitter factor", func(t *testing.T) { + conf := &ConnectionConfig{} + + opt := WithRetryJitterFactor(0.2) + err := applyConnectionOptions(conf, opt) + assert.NoError(t, err) + assert.Equal(t, 0.2, conf.Retry.JitterFactor) + + // negative disallowed + opt = WithRetryJitterFactor(-1) + err = applyConnectionOptions(conf, opt) + assert.ErrorIs(t, err, errors.InvalidRetryJitterFactor) + assert.ErrorContains(t, err, "jitter factor must be between 0.0 and 1.0") + + // >1 disallowed + opt = WithRetryJitterFactor(1.1) + err = applyConnectionOptions(conf, opt) + assert.ErrorIs(t, err, errors.InvalidRetryJitterFactor) + }) } -func TestWithRetryAttempts(t *testing.T) { - conf := &ConnectionConfig{} - opt := WithRetryMaxRetries(3) - err := applyConnectionOptions(conf, opt) - assert.NoError(t, err) - assert.Equal(t, 3, conf.Retry.MaxRetries) +// Validation Tests - // zero allowed - opt = WithRetryMaxRetries(0) - err = applyConnectionOptions(conf, opt) - assert.NoError(t, err) - - // negative disallowed - opt = WithRetryMaxRetries(-10) - err = applyConnectionOptions(conf, opt) - assert.ErrorIs(t, err, errors.InvalidRetryMaxRetries) - assert.ErrorContains(t, err, "max retry count cannot be negative") -} - -func TestWithRetryInitialDelay(t *testing.T) { - conf := &ConnectionConfig{} - opt := WithRetryInitialDelay(10 * time.Second) - err := applyConnectionOptions(conf, opt) - assert.NoError(t, err) - assert.Equal(t, 10*time.Second, conf.Retry.InitialDelay) - - // zero disallowed - opt = WithRetryInitialDelay(0 * time.Second) - err = applyConnectionOptions(conf, opt) - assert.ErrorIs(t, err, errors.InvalidRetryInitialDelay) - assert.ErrorContains(t, err, "initial delay must be positive") - - // negative disallowed - opt = WithRetryInitialDelay(-10 * time.Second) - err = applyConnectionOptions(conf, opt) - assert.ErrorIs(t, err, errors.InvalidRetryInitialDelay) -} - -func TestWithRetryMaxDelay(t *testing.T) { - conf := &ConnectionConfig{} - opt := WithRetryMaxDelay(10 * time.Second) - err := applyConnectionOptions(conf, opt) - assert.NoError(t, err) - assert.Equal(t, 10*time.Second, conf.Retry.MaxDelay) - - // zero disallowed - opt = WithRetryMaxDelay(0 * time.Second) - err = applyConnectionOptions(conf, opt) - assert.ErrorIs(t, err, errors.InvalidRetryMaxDelay) - assert.ErrorContains(t, err, "max delay must be positive") - - // negative disallowed - opt = WithRetryMaxDelay(-10 * time.Second) - err = applyConnectionOptions(conf, opt) - assert.ErrorIs(t, err, errors.InvalidRetryMaxDelay) -} - -func TestWithRetryJitterFactor(t *testing.T) { - conf := &ConnectionConfig{} - - opt := WithRetryJitterFactor(0.2) - err := applyConnectionOptions(conf, opt) - assert.NoError(t, err) - assert.Equal(t, 0.2, conf.Retry.JitterFactor) - - // negative disallowed - opt = WithRetryJitterFactor(-1) - err = applyConnectionOptions(conf, opt) - assert.ErrorIs(t, err, errors.InvalidRetryJitterFactor) - assert.ErrorContains(t, err, "jitter factor must be between 0.0 and 1.0") - - // >1 disallowed - opt = WithRetryJitterFactor(1.1) - err = applyConnectionOptions(conf, opt) - assert.ErrorIs(t, err, errors.InvalidRetryJitterFactor) -} - -// Config Validation Tests - -func TestValidateConfig(t *testing.T) { +func TestValidateConnectionConfig(t *testing.T) { cases := []struct { name string conf ConnectionConfig diff --git a/config_pool_test.go b/config_pool_test.go new file mode 100644 index 0000000..7c066ea --- /dev/null +++ b/config_pool_test.go @@ -0,0 +1,145 @@ +package honeybee + +import ( + "git.wisehodl.dev/jay/go-honeybee/errors" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestNewPoolConfig(t *testing.T) { + conf, err := NewPoolConfig() + assert.NoError(t, err) + + assert.Equal(t, conf, &PoolConfig{ + IdleTimeout: 20 * time.Second, + ConnectionConfig: nil, + }) + + // errors propagate + _, err = NewPoolConfig(WithIdleTimeout(-1)) + assert.Error(t, err) +} + +func TestDefaultPoolConfig(t *testing.T) { + conf := GetDefaultPoolConfig() + + assert.Equal(t, conf, &PoolConfig{ + IdleTimeout: 20 * time.Second, + ConnectionConfig: nil, + }) +} + +func TestApplyPoolOptions(t *testing.T) { + conf := &PoolConfig{} + err := applyPoolOptions( + conf, + WithIdleTimeout(15), + WithConnectionConfig(&ConnectionConfig{}), + ) + + assert.NoError(t, err) + assert.Equal(t, time.Duration(15), conf.IdleTimeout) + assert.Equal(t, 0*time.Second, conf.ConnectionConfig.WriteTimeout) + + // errors propagate + err = applyPoolOptions( + conf, + WithIdleTimeout(-1), + ) + + assert.ErrorIs(t, err, errors.InvalidIdleTimeout) +} + +func TestWithIdleTimeout(t *testing.T) { + conf := &PoolConfig{} + opt := WithIdleTimeout(30) + err := applyPoolOptions(conf, opt) + assert.NoError(t, err) + assert.Equal(t, conf.IdleTimeout, time.Duration(30)) + + // zero allowed + conf = &PoolConfig{} + opt = WithIdleTimeout(0) + err = applyPoolOptions(conf, opt) + assert.NoError(t, err) + assert.Equal(t, conf.IdleTimeout, time.Duration(0)) + + // negative disallowed + conf = &PoolConfig{} + opt = WithIdleTimeout(-30) + err = applyPoolOptions(conf, opt) + assert.ErrorIs(t, err, errors.InvalidIdleTimeout) + assert.ErrorContains(t, err, "idle timeout cannot be negative") +} + +func TestWithConnectionConfig(t *testing.T) { + conf := &PoolConfig{} + opt := WithConnectionConfig(&ConnectionConfig{WriteTimeout: 1 * time.Second}) + err := applyPoolOptions(conf, opt) + assert.NoError(t, err) + assert.NotNil(t, conf.ConnectionConfig) + assert.Equal(t, 1*time.Second, conf.ConnectionConfig.WriteTimeout) + + // invalid config is rejected + conf = &PoolConfig{} + opt = WithConnectionConfig(&ConnectionConfig{WriteTimeout: -1 * time.Second}) + err = applyPoolOptions(conf, opt) + assert.Error(t, err) +} + +func TestValidatePoolConfig(t *testing.T) { + cases := []struct { + name string + conf PoolConfig + wantErr error + wantErrText string + }{ + { + name: "valid empty", + conf: *&PoolConfig{}, + }, + { + name: "valid defaults", + conf: *GetDefaultPoolConfig(), + }, + { + name: "valid complete", + conf: PoolConfig{ + IdleTimeout: 15 * time.Second, + ConnectionConfig: &ConnectionConfig{}, + }, + }, + { + name: "invalid connection config", + conf: PoolConfig{ + ConnectionConfig: &ConnectionConfig{ + Retry: &RetryConfig{ + InitialDelay: 10 * time.Second, + MaxDelay: 1 * time.Second, + }, + }, + }, + wantErrText: "initial delay may not exceed maximum delay", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := validatePoolConfig(&tc.conf) + + if tc.wantErr != nil || tc.wantErrText != "" { + if tc.wantErr != nil { + assert.ErrorIs(t, err, tc.wantErr) + } + + if tc.wantErrText != "" { + assert.ErrorContains(t, err, tc.wantErrText) + } + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/errors/errors.go b/errors/errors.go index 0ba964c..9e38c89 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -8,8 +8,8 @@ var ( InvalidProtocol = errors.New("URL must use ws:// or wss:// scheme") // Configuration Errors - InvalidReadTimeout = errors.New("read timeout must be positive") - InvalidWriteTimeout = errors.New("write timeout must be positive") + InvalidIdleTimeout = errors.New("idle timeout cannot be negative") + InvalidWriteTimeout = errors.New("write timeout cannot be negative") InvalidRetryMaxRetries = errors.New("max retry count cannot be negative") InvalidRetryInitialDelay = errors.New("initial delay must be positive") InvalidRetryMaxDelay = errors.New("max delay must be positive")