backend/pkg/library/cassandra/lock_test.go

504 lines
12 KiB
Go
Raw Permalink Normal View History

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