idempotency_key_hash 和 request_fingerprint 列为 VARCHAR(64), 而 uniqueTestValue 生成的字符串含完整测试名可能超过 64 字符。 新增 hashedTestValue 辅助函数对测试值做 SHA-256 哈希, 与生产逻辑一致且严格符合列宽限制。
151 lines
4.6 KiB
Go
151 lines
4.6 KiB
Go
//go:build integration
|
|
|
|
package repository
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// hashedTestValue returns a unique SHA-256 hex string (64 chars) that fits VARCHAR(64) columns.
|
|
func hashedTestValue(t *testing.T, prefix string) string {
|
|
t.Helper()
|
|
sum := sha256.Sum256([]byte(uniqueTestValue(t, prefix)))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func TestIdempotencyRepo_CreateProcessing_CompeteSameKey(t *testing.T) {
|
|
tx := testTx(t)
|
|
repo := &idempotencyRepository{sql: tx}
|
|
ctx := context.Background()
|
|
|
|
now := time.Now().UTC()
|
|
record := &service.IdempotencyRecord{
|
|
Scope: uniqueTestValue(t, "idem-scope-create"),
|
|
IdempotencyKeyHash: hashedTestValue(t, "idem-hash"),
|
|
RequestFingerprint: hashedTestValue(t, "idem-fp"),
|
|
Status: service.IdempotencyStatusProcessing,
|
|
LockedUntil: ptrTime(now.Add(30 * time.Second)),
|
|
ExpiresAt: now.Add(24 * time.Hour),
|
|
}
|
|
owner, err := repo.CreateProcessing(ctx, record)
|
|
require.NoError(t, err)
|
|
require.True(t, owner)
|
|
require.NotZero(t, record.ID)
|
|
|
|
duplicate := &service.IdempotencyRecord{
|
|
Scope: record.Scope,
|
|
IdempotencyKeyHash: record.IdempotencyKeyHash,
|
|
RequestFingerprint: hashedTestValue(t, "idem-fp-other"),
|
|
Status: service.IdempotencyStatusProcessing,
|
|
LockedUntil: ptrTime(now.Add(30 * time.Second)),
|
|
ExpiresAt: now.Add(24 * time.Hour),
|
|
}
|
|
owner, err = repo.CreateProcessing(ctx, duplicate)
|
|
require.NoError(t, err)
|
|
require.False(t, owner, "same scope+key hash should be de-duplicated")
|
|
}
|
|
|
|
func TestIdempotencyRepo_TryReclaim_StatusAndLockWindow(t *testing.T) {
|
|
tx := testTx(t)
|
|
repo := &idempotencyRepository{sql: tx}
|
|
ctx := context.Background()
|
|
|
|
now := time.Now().UTC()
|
|
record := &service.IdempotencyRecord{
|
|
Scope: uniqueTestValue(t, "idem-scope-reclaim"),
|
|
IdempotencyKeyHash: hashedTestValue(t, "idem-hash-reclaim"),
|
|
RequestFingerprint: hashedTestValue(t, "idem-fp-reclaim"),
|
|
Status: service.IdempotencyStatusProcessing,
|
|
LockedUntil: ptrTime(now.Add(10 * time.Second)),
|
|
ExpiresAt: now.Add(24 * time.Hour),
|
|
}
|
|
owner, err := repo.CreateProcessing(ctx, record)
|
|
require.NoError(t, err)
|
|
require.True(t, owner)
|
|
|
|
require.NoError(t, repo.MarkFailedRetryable(
|
|
ctx,
|
|
record.ID,
|
|
"RETRYABLE_FAILURE",
|
|
now.Add(-2*time.Second),
|
|
now.Add(24*time.Hour),
|
|
))
|
|
|
|
newLockedUntil := now.Add(20 * time.Second)
|
|
reclaimed, err := repo.TryReclaim(
|
|
ctx,
|
|
record.ID,
|
|
service.IdempotencyStatusFailedRetryable,
|
|
now,
|
|
newLockedUntil,
|
|
now.Add(24*time.Hour),
|
|
)
|
|
require.NoError(t, err)
|
|
require.True(t, reclaimed, "failed_retryable + expired lock should allow reclaim")
|
|
|
|
got, err := repo.GetByScopeAndKeyHash(ctx, record.Scope, record.IdempotencyKeyHash)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
require.Equal(t, service.IdempotencyStatusProcessing, got.Status)
|
|
require.NotNil(t, got.LockedUntil)
|
|
require.True(t, got.LockedUntil.After(now))
|
|
|
|
require.NoError(t, repo.MarkFailedRetryable(
|
|
ctx,
|
|
record.ID,
|
|
"RETRYABLE_FAILURE",
|
|
now.Add(20*time.Second),
|
|
now.Add(24*time.Hour),
|
|
))
|
|
|
|
reclaimed, err = repo.TryReclaim(
|
|
ctx,
|
|
record.ID,
|
|
service.IdempotencyStatusFailedRetryable,
|
|
now,
|
|
now.Add(40*time.Second),
|
|
now.Add(24*time.Hour),
|
|
)
|
|
require.NoError(t, err)
|
|
require.False(t, reclaimed, "within lock window should not reclaim")
|
|
}
|
|
|
|
func TestIdempotencyRepo_StatusTransition_ToSucceeded(t *testing.T) {
|
|
tx := testTx(t)
|
|
repo := &idempotencyRepository{sql: tx}
|
|
ctx := context.Background()
|
|
|
|
now := time.Now().UTC()
|
|
record := &service.IdempotencyRecord{
|
|
Scope: uniqueTestValue(t, "idem-scope-success"),
|
|
IdempotencyKeyHash: hashedTestValue(t, "idem-hash-success"),
|
|
RequestFingerprint: hashedTestValue(t, "idem-fp-success"),
|
|
Status: service.IdempotencyStatusProcessing,
|
|
LockedUntil: ptrTime(now.Add(10 * time.Second)),
|
|
ExpiresAt: now.Add(24 * time.Hour),
|
|
}
|
|
owner, err := repo.CreateProcessing(ctx, record)
|
|
require.NoError(t, err)
|
|
require.True(t, owner)
|
|
|
|
require.NoError(t, repo.MarkSucceeded(ctx, record.ID, 200, `{"ok":true}`, now.Add(24*time.Hour)))
|
|
|
|
got, err := repo.GetByScopeAndKeyHash(ctx, record.Scope, record.IdempotencyKeyHash)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, got)
|
|
require.Equal(t, service.IdempotencyStatusSucceeded, got.Status)
|
|
require.NotNil(t, got.ResponseStatus)
|
|
require.Equal(t, 200, *got.ResponseStatus)
|
|
require.NotNil(t, got.ResponseBody)
|
|
require.Equal(t, `{"ok":true}`, *got.ResponseBody)
|
|
require.Nil(t, got.LockedUntil)
|
|
}
|
|
|