package cassandra import ( "errors" "testing" "time" "github.com/stretchr/testify/assert" ) func TestWithLockTTL(t *testing.T) { tests := []struct { name string duration time.Duration wantTTL int description string }{ { name: "30 seconds TTL", duration: 30 * time.Second, wantTTL: 30, description: "should set TTL to 30 seconds", }, { name: "1 minute TTL", duration: 1 * time.Minute, wantTTL: 60, description: "should set TTL to 60 seconds", }, { name: "5 minutes TTL", duration: 5 * time.Minute, wantTTL: 300, description: "should set TTL to 300 seconds", }, { name: "1 hour TTL", duration: 1 * time.Hour, wantTTL: 3600, description: "should set TTL to 3600 seconds", }, { name: "zero duration", duration: 0, wantTTL: 0, description: "should set TTL to 0", }, { name: "negative duration", duration: -10 * time.Second, wantTTL: -10, description: "should set TTL to negative value", }, { name: "fractional seconds", duration: 1500 * time.Millisecond, wantTTL: 1, description: "should round down fractional seconds", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { opt := WithLockTTL(tt.duration) options := &lockOptions{} opt(options) assert.Equal(t, tt.wantTTL, options.ttlSeconds, tt.description) }) } } func TestWithNoLockExpire(t *testing.T) { t.Run("should set TTL to 0", func(t *testing.T) { opt := WithNoLockExpire() options := &lockOptions{ttlSeconds: 30} // 先設置一個值 opt(options) assert.Equal(t, 0, options.ttlSeconds) }) t.Run("should override existing TTL", func(t *testing.T) { opt := WithNoLockExpire() options := &lockOptions{ttlSeconds: 100} opt(options) assert.Equal(t, 0, options.ttlSeconds) }) } func TestLockOptions_Combination(t *testing.T) { tests := []struct { name string opts []LockOption wantTTL int }{ { name: "WithLockTTL then WithNoLockExpire", opts: []LockOption{WithLockTTL(60 * time.Second), WithNoLockExpire()}, wantTTL: 0, // WithNoLockExpire should override }, { name: "WithNoLockExpire then WithLockTTL", opts: []LockOption{WithNoLockExpire(), WithLockTTL(60 * time.Second)}, wantTTL: 60, // WithLockTTL should override }, { name: "multiple WithLockTTL calls", opts: []LockOption{WithLockTTL(30 * time.Second), WithLockTTL(60 * time.Second)}, wantTTL: 60, // Last one wins }, { name: "multiple WithNoLockExpire calls", opts: []LockOption{WithNoLockExpire(), WithNoLockExpire()}, wantTTL: 0, }, { name: "empty options should use default", opts: []LockOption{}, wantTTL: defaultLockTTLSec, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { options := &lockOptions{ttlSeconds: defaultLockTTLSec} for _, opt := range tt.opts { opt(options) } assert.Equal(t, tt.wantTTL, options.ttlSeconds) }) } } func TestIsLockFailed(t *testing.T) { tests := []struct { name string err error want bool }{ { name: "Error with CONFLICT code and correct message", err: NewError(ErrCodeConflict, "acquire lock failed"), want: true, }, { name: "Error with CONFLICT code and correct message with table", err: NewError(ErrCodeConflict, "acquire lock failed").WithTable("locks"), want: true, }, { name: "Error with CONFLICT code but wrong message", err: NewError(ErrCodeConflict, "different message"), want: false, }, { name: "Error with NOT_FOUND code and correct message", err: NewError(ErrCodeNotFound, "acquire lock failed"), want: false, }, { name: "Error with INVALID_INPUT code", err: ErrInvalidInput, want: false, }, { name: "wrapped Error with CONFLICT code and correct message", err: NewError(ErrCodeConflict, "acquire lock failed"). WithError(errors.New("underlying error")), want: true, }, { name: "standard error", err: errors.New("standard error"), want: false, }, { name: "nil error", err: nil, want: false, }, { name: "Error with CONFLICT code but empty message", err: NewError(ErrCodeConflict, ""), want: false, }, { name: "Error with CONFLICT code and similar but different message", err: NewError(ErrCodeConflict, "acquire lock failed!"), want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsLockFailed(tt.err) assert.Equal(t, tt.want, result) }) } } func TestLockConstants(t *testing.T) { tests := []struct { name string constant interface{} expected interface{} }{ { name: "defaultLockTTLSec should be 30", constant: defaultLockTTLSec, expected: 30, }, { name: "defaultLockRetry should be 3", constant: defaultLockRetry, expected: 3, }, { name: "lockBaseDelay should be 100ms", constant: lockBaseDelay, expected: 100 * time.Millisecond, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, tt.constant) }) } } func TestLockOptions_DefaultValues(t *testing.T) { t.Run("default lockOptions should have default TTL", func(t *testing.T) { options := &lockOptions{ttlSeconds: defaultLockTTLSec} assert.Equal(t, defaultLockTTLSec, options.ttlSeconds) }) t.Run("lockOptions with zero TTL", func(t *testing.T) { options := &lockOptions{ttlSeconds: 0} assert.Equal(t, 0, options.ttlSeconds) }) t.Run("lockOptions with negative TTL", func(t *testing.T) { options := &lockOptions{ttlSeconds: -1} assert.Equal(t, -1, options.ttlSeconds) }) } func TestTryLock_ErrorScenarios(t *testing.T) { tests := []struct { name string description string // 注意:實際的 TryLock 測試需要 mock session 或實際的資料庫連接 // 這裡只是定義測試結構 }{ { name: "successful lock acquisition", description: "should return nil when lock is successfully acquired", }, { name: "lock already exists", description: "should return CONFLICT error when lock already exists", }, { name: "database error", description: "should return INVALID_INPUT error with underlying error when database operation fails", }, { name: "context cancellation", description: "should respect context cancellation", }, { name: "with custom TTL", description: "should use custom TTL when provided", }, { name: "with no expire", description: "should not set TTL when WithNoLockExpire is used", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 注意:這需要 mock session 或實際的資料庫連接 // 在實際測試中,需要使用 mock 或 testcontainers _ = tt }) } } func TestUnLock_ErrorScenarios(t *testing.T) { tests := []struct { name string description string // 注意:實際的 UnLock 測試需要 mock session 或實際的資料庫連接 // 這裡只是定義測試結構 }{ { name: "successful unlock", description: "should return nil when lock is successfully released", }, { name: "lock not found", description: "should retry when lock is not found", }, { name: "database error", description: "should retry on database error", }, { name: "max retries exceeded", description: "should return error after max retries", }, { name: "context cancellation", description: "should respect context cancellation", }, { name: "exponential backoff", description: "should use exponential backoff between retries", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 注意:這需要 mock session 或實際的資料庫連接 // 在實際測試中,需要使用 mock 或 testcontainers _ = tt }) } } func TestLockOption_Type(t *testing.T) { t.Run("WithLockTTL should return LockOption", func(t *testing.T) { opt := WithLockTTL(30 * time.Second) assert.NotNil(t, opt) // 驗證它是一個函數 var lockOpt LockOption = opt assert.NotNil(t, lockOpt) }) t.Run("WithNoLockExpire should return LockOption", func(t *testing.T) { opt := WithNoLockExpire() assert.NotNil(t, opt) // 驗證它是一個函數 var lockOpt LockOption = opt assert.NotNil(t, lockOpt) }) } func TestLockOptions_ApplyOrder(t *testing.T) { t.Run("last option should win", func(t *testing.T) { options := &lockOptions{ttlSeconds: defaultLockTTLSec} WithLockTTL(60 * time.Second)(options) assert.Equal(t, 60, options.ttlSeconds) WithNoLockExpire()(options) assert.Equal(t, 0, options.ttlSeconds) WithLockTTL(120 * time.Second)(options) assert.Equal(t, 120, options.ttlSeconds) }) } func TestIsLockFailed_EdgeCases(t *testing.T) { tests := []struct { name string err error want bool }{ { name: "Error with CONFLICT code, correct message, and underlying error", err: NewError(ErrCodeConflict, "acquire lock failed"). WithTable("locks"). WithError(errors.New("database error")), want: true, }, { name: "Error with CONFLICT code but message with extra spaces", err: NewError(ErrCodeConflict, " acquire lock failed "), want: false, }, { name: "Error with CONFLICT code but message with different case", err: NewError(ErrCodeConflict, "Acquire Lock Failed"), want: false, }, { name: "chained errors with CONFLICT", err: func() error { err1 := NewError(ErrCodeConflict, "acquire lock failed") err2 := errors.New("wrapped") return errors.Join(err1, err2) }(), want: true, // errors.Join preserves Error type and errors.As can find it }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsLockFailed(tt.err) assert.Equal(t, tt.want, result) }) } } func TestLockOptions_ZeroValue(t *testing.T) { t.Run("zero value lockOptions", func(t *testing.T) { var options lockOptions assert.Equal(t, 0, options.ttlSeconds) }) t.Run("apply option to zero value", func(t *testing.T) { var options lockOptions WithLockTTL(30 * time.Second)(&options) assert.Equal(t, 30, options.ttlSeconds) }) } func TestLockRetryDelay(t *testing.T) { t.Run("verify exponential backoff calculation", func(t *testing.T) { // 驗證重試延遲的計算邏輯 // 100ms → 200ms → 400ms expectedDelays := []time.Duration{ lockBaseDelay * time.Duration(1<<0), // 100ms * 1 = 100ms lockBaseDelay * time.Duration(1<<1), // 100ms * 2 = 200ms lockBaseDelay * time.Duration(1<<2), // 100ms * 4 = 400ms } assert.Equal(t, 100*time.Millisecond, expectedDelays[0]) assert.Equal(t, 200*time.Millisecond, expectedDelays[1]) assert.Equal(t, 400*time.Millisecond, expectedDelays[2]) }) } func TestLockOption_InterfaceCompliance(t *testing.T) { t.Run("LockOption should be a function type", func(t *testing.T) { // 驗證 LockOption 是一個函數類型 var fn func(*lockOptions) = WithLockTTL(30 * time.Second) assert.NotNil(t, fn) }) t.Run("LockOption can be assigned from WithLockTTL", func(t *testing.T) { var opt LockOption = WithLockTTL(30 * time.Second) assert.NotNil(t, opt) }) t.Run("LockOption can be assigned from WithNoLockExpire", func(t *testing.T) { var opt LockOption = WithNoLockExpire() assert.NotNil(t, opt) }) } func TestLockOptions_RealWorldScenarios(t *testing.T) { tests := []struct { name string scenario func(*lockOptions) wantTTL int }{ { name: "short-lived lock (5 seconds)", scenario: func(o *lockOptions) { WithLockTTL(5 * time.Second)(o) }, wantTTL: 5, }, { name: "medium-lived lock (5 minutes)", scenario: func(o *lockOptions) { WithLockTTL(5 * time.Minute)(o) }, wantTTL: 300, }, { name: "long-lived lock (1 hour)", scenario: func(o *lockOptions) { WithLockTTL(1 * time.Hour)(o) }, wantTTL: 3600, }, { name: "permanent lock", scenario: func(o *lockOptions) { WithNoLockExpire()(o) }, wantTTL: 0, }, { name: "default lock", scenario: func(o *lockOptions) { // 不應用任何選項,使用預設值 }, wantTTL: defaultLockTTLSec, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { options := &lockOptions{ttlSeconds: defaultLockTTLSec} tt.scenario(options) assert.Equal(t, tt.wantTTL, options.ttlSeconds) }) } }