package cassandra import ( "testing" "time" "github.com/gocql/gocql" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestOption_DefaultConfig(t *testing.T) { t.Run("defaultConfig should return valid config with all defaults", func(t *testing.T) { cfg := defaultConfig() require.NotNil(t, cfg) assert.Equal(t, defaultPort, cfg.Port) assert.Equal(t, defaultConsistency, cfg.Consistency) assert.Equal(t, defaultTimeoutSec, cfg.ConnectTimeoutSec) assert.Equal(t, defaultNumConns, cfg.NumConns) assert.Equal(t, defaultMaxRetries, cfg.MaxRetries) assert.Equal(t, defaultRetryMinInterval, cfg.RetryMinInterval) assert.Equal(t, defaultRetryMaxInterval, cfg.RetryMaxInterval) assert.Equal(t, defaultReconnectInitialInterval, cfg.ReconnectInitialInterval) assert.Equal(t, defaultReconnectMaxInterval, cfg.ReconnectMaxInterval) assert.Equal(t, defaultCqlVersion, cfg.CQLVersion) assert.Empty(t, cfg.Hosts) assert.Empty(t, cfg.Keyspace) assert.Empty(t, cfg.Username) assert.Empty(t, cfg.Password) assert.False(t, cfg.UseAuth) }) } func TestWithHosts(t *testing.T) { tests := []struct { name string hosts []string expected []string }{ { name: "single host", hosts: []string{"localhost"}, expected: []string{"localhost"}, }, { name: "multiple hosts", hosts: []string{"localhost", "127.0.0.1", "192.168.1.1"}, expected: []string{"localhost", "127.0.0.1", "192.168.1.1"}, }, { name: "empty hosts", hosts: []string{}, expected: []string{}, }, { name: "host with port", hosts: []string{"localhost:9042"}, expected: []string{"localhost:9042"}, }, { name: "host with domain", hosts: []string{"cassandra.example.com"}, expected: []string{"cassandra.example.com"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithHosts(tt.hosts...) opt(cfg) assert.Equal(t, tt.expected, cfg.Hosts) }) } } func TestWithPort(t *testing.T) { tests := []struct { name string port int expected int }{ { name: "default port", port: 9042, expected: 9042, }, { name: "custom port", port: 9043, expected: 9043, }, { name: "zero port", port: 0, expected: 0, }, { name: "negative port", port: -1, expected: -1, }, { name: "high port number", port: 65535, expected: 65535, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithPort(tt.port) opt(cfg) assert.Equal(t, tt.expected, cfg.Port) }) } } func TestWithKeyspace(t *testing.T) { tests := []struct { name string keyspace string expected string }{ { name: "valid keyspace", keyspace: "my_keyspace", expected: "my_keyspace", }, { name: "empty keyspace", keyspace: "", expected: "", }, { name: "keyspace with underscore", keyspace: "test_keyspace_1", expected: "test_keyspace_1", }, { name: "keyspace with numbers", keyspace: "keyspace123", expected: "keyspace123", }, { name: "long keyspace name", keyspace: "very_long_keyspace_name_that_might_exist", expected: "very_long_keyspace_name_that_might_exist", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithKeyspace(tt.keyspace) opt(cfg) assert.Equal(t, tt.expected, cfg.Keyspace) }) } } func TestWithAuth(t *testing.T) { tests := []struct { name string username string password string expectedUser string expectedPass string expectedUseAuth bool }{ { name: "valid credentials", username: "admin", password: "password123", expectedUser: "admin", expectedPass: "password123", expectedUseAuth: true, }, { name: "empty username", username: "", password: "password", expectedUser: "", expectedPass: "password", expectedUseAuth: true, }, { name: "empty password", username: "admin", password: "", expectedUser: "admin", expectedPass: "", expectedUseAuth: true, }, { name: "both empty", username: "", password: "", expectedUser: "", expectedPass: "", expectedUseAuth: true, }, { name: "special characters in password", username: "user", password: "p@ssw0rd!#$%", expectedUser: "user", expectedPass: "p@ssw0rd!#$%", expectedUseAuth: true, }, { name: "long username and password", username: "very_long_username_that_might_exist", password: "very_long_password_that_might_exist", expectedUser: "very_long_username_that_might_exist", expectedPass: "very_long_password_that_might_exist", expectedUseAuth: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithAuth(tt.username, tt.password) opt(cfg) assert.Equal(t, tt.expectedUser, cfg.Username) assert.Equal(t, tt.expectedPass, cfg.Password) assert.Equal(t, tt.expectedUseAuth, cfg.UseAuth) }) } } func TestWithConsistency(t *testing.T) { tests := []struct { name string consistency gocql.Consistency expected gocql.Consistency }{ { name: "Quorum consistency", consistency: gocql.Quorum, expected: gocql.Quorum, }, { name: "One consistency", consistency: gocql.One, expected: gocql.One, }, { name: "All consistency", consistency: gocql.All, expected: gocql.All, }, { name: "Any consistency", consistency: gocql.Any, expected: gocql.Any, }, { name: "LocalQuorum consistency", consistency: gocql.LocalQuorum, expected: gocql.LocalQuorum, }, { name: "EachQuorum consistency", consistency: gocql.EachQuorum, expected: gocql.EachQuorum, }, { name: "LocalOne consistency", consistency: gocql.LocalOne, expected: gocql.LocalOne, }, { name: "Two consistency", consistency: gocql.Two, expected: gocql.Two, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithConsistency(tt.consistency) opt(cfg) assert.Equal(t, tt.expected, cfg.Consistency) }) } } func TestWithConnectTimeoutSec(t *testing.T) { tests := []struct { name string timeout int expected int }{ { name: "valid timeout", timeout: 10, expected: 10, }, { name: "zero timeout should use default", timeout: 0, expected: defaultTimeoutSec, }, { name: "negative timeout should use default", timeout: -1, expected: defaultTimeoutSec, }, { name: "large timeout", timeout: 300, expected: 300, }, { name: "small timeout", timeout: 1, expected: 1, }, { name: "very large timeout", timeout: 3600, expected: 3600, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithConnectTimeoutSec(tt.timeout) opt(cfg) assert.Equal(t, tt.expected, cfg.ConnectTimeoutSec) }) } } func TestWithNumConns(t *testing.T) { tests := []struct { name string numConns int expected int }{ { name: "valid numConns", numConns: 10, expected: 10, }, { name: "zero numConns should use default", numConns: 0, expected: defaultNumConns, }, { name: "negative numConns should use default", numConns: -1, expected: defaultNumConns, }, { name: "large numConns", numConns: 100, expected: 100, }, { name: "small numConns", numConns: 1, expected: 1, }, { name: "very large numConns", numConns: 1000, expected: 1000, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithNumConns(tt.numConns) opt(cfg) assert.Equal(t, tt.expected, cfg.NumConns) }) } } func TestWithMaxRetries(t *testing.T) { tests := []struct { name string maxRetries int expected int }{ { name: "valid maxRetries", maxRetries: 3, expected: 3, }, { name: "zero maxRetries should use default", maxRetries: 0, expected: defaultMaxRetries, }, { name: "negative maxRetries should use default", maxRetries: -1, expected: defaultMaxRetries, }, { name: "large maxRetries", maxRetries: 10, expected: 10, }, { name: "small maxRetries", maxRetries: 1, expected: 1, }, { name: "very large maxRetries", maxRetries: 100, expected: 100, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithMaxRetries(tt.maxRetries) opt(cfg) assert.Equal(t, tt.expected, cfg.MaxRetries) }) } } func TestWithRetryMinInterval(t *testing.T) { tests := []struct { name string duration time.Duration expected time.Duration }{ { name: "valid duration", duration: 1 * time.Second, expected: 1 * time.Second, }, { name: "zero duration should use default", duration: 0, expected: defaultRetryMinInterval, }, { name: "negative duration should use default", duration: -1 * time.Second, expected: defaultRetryMinInterval, }, { name: "milliseconds", duration: 500 * time.Millisecond, expected: 500 * time.Millisecond, }, { name: "minutes", duration: 5 * time.Minute, expected: 5 * time.Minute, }, { name: "hours", duration: 1 * time.Hour, expected: 1 * time.Hour, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithRetryMinInterval(tt.duration) opt(cfg) assert.Equal(t, tt.expected, cfg.RetryMinInterval) }) } } func TestWithRetryMaxInterval(t *testing.T) { tests := []struct { name string duration time.Duration expected time.Duration }{ { name: "valid duration", duration: 30 * time.Second, expected: 30 * time.Second, }, { name: "zero duration should use default", duration: 0, expected: defaultRetryMaxInterval, }, { name: "negative duration should use default", duration: -1 * time.Second, expected: defaultRetryMaxInterval, }, { name: "milliseconds", duration: 1000 * time.Millisecond, expected: 1000 * time.Millisecond, }, { name: "minutes", duration: 10 * time.Minute, expected: 10 * time.Minute, }, { name: "hours", duration: 2 * time.Hour, expected: 2 * time.Hour, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithRetryMaxInterval(tt.duration) opt(cfg) assert.Equal(t, tt.expected, cfg.RetryMaxInterval) }) } } func TestWithReconnectInitialInterval(t *testing.T) { tests := []struct { name string duration time.Duration expected time.Duration }{ { name: "valid duration", duration: 1 * time.Second, expected: 1 * time.Second, }, { name: "zero duration should use default", duration: 0, expected: defaultReconnectInitialInterval, }, { name: "negative duration should use default", duration: -1 * time.Second, expected: defaultReconnectInitialInterval, }, { name: "milliseconds", duration: 500 * time.Millisecond, expected: 500 * time.Millisecond, }, { name: "minutes", duration: 2 * time.Minute, expected: 2 * time.Minute, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithReconnectInitialInterval(tt.duration) opt(cfg) assert.Equal(t, tt.expected, cfg.ReconnectInitialInterval) }) } } func TestWithReconnectMaxInterval(t *testing.T) { tests := []struct { name string duration time.Duration expected time.Duration }{ { name: "valid duration", duration: 60 * time.Second, expected: 60 * time.Second, }, { name: "zero duration should use default", duration: 0, expected: defaultReconnectMaxInterval, }, { name: "negative duration should use default", duration: -1 * time.Second, expected: defaultReconnectMaxInterval, }, { name: "milliseconds", duration: 5000 * time.Millisecond, expected: 5000 * time.Millisecond, }, { name: "minutes", duration: 5 * time.Minute, expected: 5 * time.Minute, }, { name: "hours", duration: 1 * time.Hour, expected: 1 * time.Hour, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithReconnectMaxInterval(tt.duration) opt(cfg) assert.Equal(t, tt.expected, cfg.ReconnectMaxInterval) }) } } func TestWithCQLVersion(t *testing.T) { tests := []struct { name string version string expected string }{ { name: "valid version", version: "3.0.0", expected: "3.0.0", }, { name: "empty version should use default", version: "", expected: defaultCqlVersion, }, { name: "version 3.1.0", version: "3.1.0", expected: "3.1.0", }, { name: "version 3.4.0", version: "3.4.0", expected: "3.4.0", }, { name: "version 4.0.0", version: "4.0.0", expected: "4.0.0", }, { name: "version with build", version: "3.0.0-beta", expected: "3.0.0-beta", }, { name: "version with snapshot", version: "3.0.0-SNAPSHOT", expected: "3.0.0-SNAPSHOT", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() opt := WithCQLVersion(tt.version) opt(cfg) assert.Equal(t, tt.expected, cfg.CQLVersion) }) } } func TestOption_Combination(t *testing.T) { tests := []struct { name string opts []Option validate func(*testing.T, *config) }{ { name: "all options", opts: []Option{ WithHosts("localhost", "127.0.0.1"), WithPort(9042), WithKeyspace("test_keyspace"), WithAuth("user", "pass"), WithConsistency(gocql.Quorum), WithConnectTimeoutSec(10), WithNumConns(10), WithMaxRetries(3), WithRetryMinInterval(1 * time.Second), WithRetryMaxInterval(30 * time.Second), WithReconnectInitialInterval(1 * time.Second), WithReconnectMaxInterval(60 * time.Second), WithCQLVersion("3.0.0"), }, validate: func(t *testing.T, c *config) { assert.Equal(t, []string{"localhost", "127.0.0.1"}, c.Hosts) assert.Equal(t, 9042, c.Port) assert.Equal(t, "test_keyspace", c.Keyspace) assert.Equal(t, "user", c.Username) assert.Equal(t, "pass", c.Password) assert.True(t, c.UseAuth) assert.Equal(t, gocql.Quorum, c.Consistency) assert.Equal(t, 10, c.ConnectTimeoutSec) assert.Equal(t, 10, c.NumConns) assert.Equal(t, 3, c.MaxRetries) assert.Equal(t, 1*time.Second, c.RetryMinInterval) assert.Equal(t, 30*time.Second, c.RetryMaxInterval) assert.Equal(t, 1*time.Second, c.ReconnectInitialInterval) assert.Equal(t, 60*time.Second, c.ReconnectMaxInterval) assert.Equal(t, "3.0.0", c.CQLVersion) }, }, { name: "minimal options", opts: []Option{ WithHosts("localhost"), }, validate: func(t *testing.T, c *config) { assert.Equal(t, []string{"localhost"}, c.Hosts) // 其他應該使用預設值 assert.Equal(t, defaultPort, c.Port) assert.Equal(t, defaultConsistency, c.Consistency) }, }, { name: "options with zero values should use defaults", opts: []Option{ WithHosts("localhost"), WithConnectTimeoutSec(0), WithNumConns(0), WithMaxRetries(0), WithRetryMinInterval(0), WithRetryMaxInterval(0), WithReconnectInitialInterval(0), WithReconnectMaxInterval(0), WithCQLVersion(""), }, validate: func(t *testing.T, c *config) { assert.Equal(t, []string{"localhost"}, c.Hosts) assert.Equal(t, defaultTimeoutSec, c.ConnectTimeoutSec) assert.Equal(t, defaultNumConns, c.NumConns) assert.Equal(t, defaultMaxRetries, c.MaxRetries) assert.Equal(t, defaultRetryMinInterval, c.RetryMinInterval) assert.Equal(t, defaultRetryMaxInterval, c.RetryMaxInterval) assert.Equal(t, defaultReconnectInitialInterval, c.ReconnectInitialInterval) assert.Equal(t, defaultReconnectMaxInterval, c.ReconnectMaxInterval) assert.Equal(t, defaultCqlVersion, c.CQLVersion) }, }, { name: "options with negative values should use defaults", opts: []Option{ WithHosts("localhost"), WithConnectTimeoutSec(-1), WithNumConns(-1), WithMaxRetries(-1), WithRetryMinInterval(-1 * time.Second), WithRetryMaxInterval(-1 * time.Second), WithReconnectInitialInterval(-1 * time.Second), WithReconnectMaxInterval(-1 * time.Second), }, validate: func(t *testing.T, c *config) { assert.Equal(t, []string{"localhost"}, c.Hosts) assert.Equal(t, defaultTimeoutSec, c.ConnectTimeoutSec) assert.Equal(t, defaultNumConns, c.NumConns) assert.Equal(t, defaultMaxRetries, c.MaxRetries) assert.Equal(t, defaultRetryMinInterval, c.RetryMinInterval) assert.Equal(t, defaultRetryMaxInterval, c.RetryMaxInterval) assert.Equal(t, defaultReconnectInitialInterval, c.ReconnectInitialInterval) assert.Equal(t, defaultReconnectMaxInterval, c.ReconnectMaxInterval) }, }, { name: "multiple options applied in sequence", opts: []Option{ WithHosts("host1"), WithHosts("host2", "host3"), // 應該覆蓋 WithPort(9042), WithPort(9043), // 應該覆蓋 }, validate: func(t *testing.T, c *config) { assert.Equal(t, []string{"host2", "host3"}, c.Hosts) assert.Equal(t, 9043, c.Port) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() for _, opt := range tt.opts { opt(cfg) } tt.validate(t, cfg) }) } } func TestOption_Type(t *testing.T) { t.Run("all options should return Option type", func(t *testing.T) { var opt Option opt = WithHosts("localhost") assert.NotNil(t, opt) opt = WithPort(9042) assert.NotNil(t, opt) opt = WithKeyspace("test") assert.NotNil(t, opt) opt = WithAuth("user", "pass") assert.NotNil(t, opt) opt = WithConsistency(gocql.Quorum) assert.NotNil(t, opt) opt = WithConnectTimeoutSec(10) assert.NotNil(t, opt) opt = WithNumConns(10) assert.NotNil(t, opt) opt = WithMaxRetries(3) assert.NotNil(t, opt) opt = WithRetryMinInterval(1 * time.Second) assert.NotNil(t, opt) opt = WithRetryMaxInterval(30 * time.Second) assert.NotNil(t, opt) opt = WithReconnectInitialInterval(1 * time.Second) assert.NotNil(t, opt) opt = WithReconnectMaxInterval(60 * time.Second) assert.NotNil(t, opt) opt = WithCQLVersion("3.0.0") assert.NotNil(t, opt) }) } func TestOption_EdgeCases(t *testing.T) { t.Run("empty option slice", func(t *testing.T) { cfg := defaultConfig() opts := []Option{} for _, opt := range opts { opt(cfg) } // 應該保持預設值 assert.Equal(t, defaultPort, cfg.Port) assert.Equal(t, defaultConsistency, cfg.Consistency) }) t.Run("zero value option function", func(t *testing.T) { cfg := defaultConfig() var opt Option // 零值的 Option 是 nil,調用會 panic,所以不應該調用 // 這裡只是驗證零值不會影響配置 _ = opt // 應該保持預設值 assert.Equal(t, defaultPort, cfg.Port) }) t.Run("very long strings", func(t *testing.T) { cfg := defaultConfig() longString := string(make([]byte, 10000)) WithKeyspace(longString)(cfg) assert.Equal(t, longString, cfg.Keyspace) WithAuth(longString, longString)(cfg) assert.Equal(t, longString, cfg.Username) assert.Equal(t, longString, cfg.Password) }) t.Run("special characters in strings", func(t *testing.T) { cfg := defaultConfig() specialChars := "!@#$%^&*()_+-=[]{}|;:,.<>?" WithKeyspace(specialChars)(cfg) assert.Equal(t, specialChars, cfg.Keyspace) WithAuth(specialChars, specialChars)(cfg) assert.Equal(t, specialChars, cfg.Username) assert.Equal(t, specialChars, cfg.Password) }) } func TestOption_RealWorldScenarios(t *testing.T) { tests := []struct { name string scenario string opts []Option validate func(*testing.T, *config) }{ { name: "production-like configuration", scenario: "typical production setup", opts: []Option{ WithHosts("cassandra1.example.com", "cassandra2.example.com", "cassandra3.example.com"), WithPort(9042), WithKeyspace("production_keyspace"), WithAuth("prod_user", "secure_password"), WithConsistency(gocql.Quorum), WithConnectTimeoutSec(30), WithNumConns(50), WithMaxRetries(5), }, validate: func(t *testing.T, c *config) { assert.Len(t, c.Hosts, 3) assert.Equal(t, 9042, c.Port) assert.Equal(t, "production_keyspace", c.Keyspace) assert.True(t, c.UseAuth) assert.Equal(t, gocql.Quorum, c.Consistency) assert.Equal(t, 30, c.ConnectTimeoutSec) assert.Equal(t, 50, c.NumConns) assert.Equal(t, 5, c.MaxRetries) }, }, { name: "development configuration", scenario: "local development setup", opts: []Option{ WithHosts("localhost"), WithKeyspace("dev_keyspace"), }, validate: func(t *testing.T, c *config) { assert.Equal(t, []string{"localhost"}, c.Hosts) assert.Equal(t, "dev_keyspace", c.Keyspace) assert.False(t, c.UseAuth) }, }, { name: "high availability configuration", scenario: "HA setup with multiple hosts", opts: []Option{ WithHosts("node1", "node2", "node3", "node4", "node5"), WithConsistency(gocql.All), WithMaxRetries(10), }, validate: func(t *testing.T, c *config) { assert.Len(t, c.Hosts, 5) assert.Equal(t, gocql.All, c.Consistency) assert.Equal(t, 10, c.MaxRetries) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := defaultConfig() for _, opt := range tt.opts { opt(cfg) } tt.validate(t, cfg) }) } }