Files
story/internal/idempotency/executor_integration_test.go
T

102 lines
3.0 KiB
Go
Raw Normal View History

2026-08-26 06:41:07 +08:00
package idempotency
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestExecutorReplaysAndRejectsHashConflict(t *testing.T) {
pool := integrationPool(t)
executor := NewExecutor(pool)
key := fmt.Sprintf("test-%d", time.Now().UnixNano())
tenantID := "10000000-0000-0000-0000-000000000001"
request := Request{
TenantID: &tenantID, ActorKey: "integration-user", Scope: "integration.create",
Key: key, RequestHash: Hash([]byte(`{"value":1}`)), ExpiresAfter: time.Hour,
}
var calls atomic.Int32
operation := func(context.Context, pgx.Tx) (Result, error) {
calls.Add(1)
return Result{StatusCode: 201, Body: json.RawMessage(`{"created":true}`)}, nil
}
first, err := executor.Execute(context.Background(), request, operation)
if err != nil || first.Replayed {
t.Fatalf("first execute = %#v, %v", first, err)
}
second, err := executor.Execute(context.Background(), request, operation)
if err != nil || !second.Replayed || string(second.Body) != `{"created": true}` {
t.Fatalf("second execute = %#v, %v", second, err)
}
if calls.Load() != 1 {
t.Fatalf("operation calls = %d, want 1", calls.Load())
}
lookedUp, found, err := executor.Lookup(context.Background(), request)
if err != nil || !found || !lookedUp.Replayed || string(lookedUp.Body) != `{"created": true}` {
t.Fatalf("lookup=%#v found=%v err=%v", lookedUp, found, err)
}
request.RequestHash = Hash([]byte(`{"value":2}`))
if _, err := executor.Execute(context.Background(), request, operation); !errors.Is(err, ErrKeyConflict) {
t.Fatalf("hash conflict error = %v, want ErrKeyConflict", err)
}
}
func TestExecutorSerializesConcurrentRequests(t *testing.T) {
pool := integrationPool(t)
executor := NewExecutor(pool)
key := fmt.Sprintf("concurrent-%d", time.Now().UnixNano())
request := Request{
ActorKey: "integration-concurrent", Scope: "integration.create", Key: key,
RequestHash: Hash([]byte(`{"value":1}`)), ExpiresAfter: time.Hour,
}
var calls atomic.Int32
operation := func(context.Context, pgx.Tx) (Result, error) {
calls.Add(1)
time.Sleep(30 * time.Millisecond)
return Result{StatusCode: 201, Body: json.RawMessage(`{"ok":true}`)}, nil
}
var waitGroup sync.WaitGroup
errorsChannel := make(chan error, 5)
for range 5 {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
_, err := executor.Execute(context.Background(), request, operation)
errorsChannel <- err
}()
}
waitGroup.Wait()
close(errorsChannel)
for err := range errorsChannel {
if err != nil {
t.Errorf("concurrent execute returned %v", err)
}
}
if calls.Load() != 1 {
t.Fatalf("operation calls = %d, want 1", calls.Load())
}
}
func integrationPool(t *testing.T) *pgxpool.Pool {
t.Helper()
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
pool, err := pgxpool.New(context.Background(), databaseURL)
if err != nil {
t.Fatalf("connect integration database: %v", err)
}
t.Cleanup(pool.Close)
return pool
}