backend/pkg/library/cassandra/option_test.go

964 lines
22 KiB
Go
Raw Normal View History

2025-11-19 05:33:06 +00:00
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)
})
}
}