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