backend/pkg/library/cassandra/option_test.go

964 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
})
}
}