112 lines
3.2 KiB
Go
112 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
|
"git.k6n.net/mats/go-cart-actor/pkg/idempotency"
|
|
"git.k6n.net/mats/go-cart-actor/pkg/flow"
|
|
"git.k6n.net/mats/go-cart-actor/pkg/order"
|
|
)
|
|
|
|
func testServer(t *testing.T) *server {
|
|
t.Helper()
|
|
reg := actor.NewMutationRegistry()
|
|
order.RegisterMutations(reg)
|
|
storage := actor.NewDiskStorage[order.OrderGrain](t.TempDir(), reg)
|
|
pool, err := actor.NewSimpleGrainPool(actor.GrainPoolConfig[order.OrderGrain]{
|
|
Hostname: "test",
|
|
Spawn: func(_ context.Context, id uint64) (actor.Grain[order.OrderGrain], error) {
|
|
return order.NewOrderGrain(id, time.Now()), nil
|
|
},
|
|
SpawnHost: func(string) (actor.Host[order.OrderGrain], error) { return nil, fmt.Errorf("none") },
|
|
Destroy: func(actor.Grain[order.OrderGrain]) error { return nil },
|
|
TTL: time.Hour,
|
|
PoolSize: 100,
|
|
|
|
MutationRegistry: reg,
|
|
Storage: nil,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(pool.Close)
|
|
|
|
applier := &orderedApplier{pool: pool, storage: storage}
|
|
provider := order.NewPassthroughProvider("legacy", "ref", 15000)
|
|
|
|
freg := flow.NewRegistry()
|
|
flow.RegisterBuiltinHooks(freg)
|
|
order.RegisterFlowActions(freg, applier, provider)
|
|
order.RegisterEmitHook(freg, nil)
|
|
engine := flow.NewEngine(freg, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
|
|
def, err := order.EmbeddedFlow("place-and-pay")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
idem, err := idempotency.Open(filepath.Join(t.TempDir(), "idem.log"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return &server{
|
|
pool: pool, applier: applier, storage: storage, reg: reg,
|
|
engine: engine, freg: freg,
|
|
flows: &flowStore{defs: map[string]*flow.Definition{def.Name: def}},
|
|
provider: provider, taxProvider: order.NewStaticTaxProvider(),
|
|
defaultFlow: def.Name, idem: idem, logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
|
|
}
|
|
}
|
|
|
|
func TestIngestOrderFromQueue(t *testing.T) {
|
|
s := testServer(t)
|
|
ctx := context.Background()
|
|
body := []byte(`{
|
|
"checkoutId":"K-1","idempotencyKey":"checkout-K-1","cartId":"cart-1",
|
|
"currency":"SEK","country":"SE","customerEmail":"customer@example.com",
|
|
"lines":[{"reference":"l1","sku":"l1","name":"Widget","quantity":1,"unitPrice":15000,"taxRate":25}],
|
|
"payment":{"provider":"klarna","reference":"ref-123","amount":15000}
|
|
}`)
|
|
|
|
if err := ingestOrderFromQueue(ctx, s, body); err != nil {
|
|
t.Fatalf("ingest: %v", err)
|
|
}
|
|
|
|
// Retrieve the created order grain. The new order ID is recorded in idempotency.Store
|
|
id, ok := s.idem.Get("checkout-K-1")
|
|
if !ok {
|
|
t.Fatalf("expected order ID to be stored in idempotency cache")
|
|
}
|
|
|
|
g, err := s.applier.Get(ctx, id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if g.Status != order.StatusCaptured {
|
|
t.Fatalf("status = %q, want captured", g.Status)
|
|
}
|
|
if g.CapturedAmount != 15000 {
|
|
t.Fatalf("captured = %d, want 15000", g.CapturedAmount)
|
|
}
|
|
|
|
// Redelivery must be idempotent — no double capture, no new payment.
|
|
if err := ingestOrderFromQueue(ctx, s, body); err != nil {
|
|
t.Fatalf("re-ingest: %v", err)
|
|
}
|
|
g2, _ := s.applier.Get(ctx, id)
|
|
if g2.CapturedAmount != 15000 {
|
|
t.Fatalf("redelivery double-captured: %d", g2.CapturedAmount)
|
|
}
|
|
if len(g2.Payments) != 1 {
|
|
t.Fatalf("redelivery created %d payments, want 1", len(g2.Payments))
|
|
}
|
|
}
|