504 lines
12 KiB
Go
504 lines
12 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|
|
|