refactor
This commit is contained in:
@@ -3,19 +3,28 @@ package order
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/mail"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/flow"
|
||||
)
|
||||
|
||||
func fullRegistry(t *testing.T, pub Publisher) *flow.Registry {
|
||||
t.Helper()
|
||||
pool := newPool(t)
|
||||
reg := flow.NewRegistry()
|
||||
flow.RegisterBuiltinHooks(reg)
|
||||
RegisterFlowActions(reg, newPool(t), NewMockProvider())
|
||||
RegisterFlowActions(reg, pool, NewMockProvider())
|
||||
RegisterInventoryReservationActions(reg, pool, nil)
|
||||
RegisterEmitHook(reg, pub)
|
||||
RegisterEmailHook(reg, pool, nil, nil)
|
||||
RegisterFulfillmentWebhookHook(reg, pool, nil)
|
||||
return reg
|
||||
}
|
||||
|
||||
@@ -30,6 +39,92 @@ func TestCapabilitiesIncludesPredicatesAndEmit(t *testing.T) {
|
||||
if !slices.Contains(caps.Hooks, "amqp_emit") {
|
||||
t.Errorf("hooks missing amqp_emit (got %v)", caps.Hooks)
|
||||
}
|
||||
if !slices.Contains(caps.Hooks, "send_email") {
|
||||
t.Errorf("hooks missing send_email (got %v)", caps.Hooks)
|
||||
}
|
||||
if !slices.Contains(caps.Hooks, "fulfillment_webhook") {
|
||||
t.Errorf("hooks missing fulfillment_webhook (got %v)", caps.Hooks)
|
||||
}
|
||||
if !slices.Contains(caps.Actions, "create_fulfillment") {
|
||||
t.Errorf("actions missing create_fulfillment (got %v)", caps.Actions)
|
||||
}
|
||||
if !slices.Contains(caps.Actions, "reserve_inventory") {
|
||||
t.Errorf("actions missing reserve_inventory (got %v)", caps.Actions)
|
||||
}
|
||||
if !slices.Contains(caps.Actions, "release_inventory") {
|
||||
t.Errorf("actions missing release_inventory (got %v)", caps.Actions)
|
||||
}
|
||||
if caps.ActionMeta["create_fulfillment"].Description == "" {
|
||||
t.Fatalf("action meta missing for create_fulfillment: %#v", caps.ActionMeta["create_fulfillment"])
|
||||
}
|
||||
if len(caps.HookMeta["fulfillment_webhook"].ExampleParams) == 0 {
|
||||
t.Fatalf("hook meta missing example params for fulfillment_webhook: %#v", caps.HookMeta["fulfillment_webhook"])
|
||||
}
|
||||
if caps.PredicateMeta["has_customer_email"].Description == "" {
|
||||
t.Fatalf("predicate meta missing for has_customer_email: %#v", caps.PredicateMeta["has_customer_email"])
|
||||
}
|
||||
}
|
||||
|
||||
type fakeInventoryReservationService struct {
|
||||
reserved []InventoryReservationRequest
|
||||
released []InventoryReservationRequest
|
||||
}
|
||||
|
||||
func (f *fakeInventoryReservationService) Reserve(_ context.Context, req InventoryReservationRequest) error {
|
||||
f.reserved = append(f.reserved, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeInventoryReservationService) Release(_ context.Context, req InventoryReservationRequest) error {
|
||||
f.released = append(f.released, req)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestReserveInventoryActionBuildsReservationRequest(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
reg := flow.NewRegistry()
|
||||
RegisterFlowActions(reg, pool, NewMockProvider())
|
||||
fake := &fakeInventoryReservationService{}
|
||||
RegisterInventoryReservationActions(reg, pool, fake)
|
||||
eng := flow.NewEngine(reg, nil)
|
||||
|
||||
const id = 703
|
||||
st := flow.NewState(id, nil)
|
||||
po := placeMsg()
|
||||
st.Vars[PlaceOrderVar] = po
|
||||
def, err := EmbeddedFlow("place-and-pay")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := eng.Run(context.Background(), def, st); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
reserve := &flow.Definition{Name: "reserve", Steps: []flow.Step{{
|
||||
Name: "reserve",
|
||||
Action: "reserve_inventory",
|
||||
Params: json.RawMessage(`{"ttlSeconds":600,"lines":[{"reference":"l1","quantity":1,"locationId":"se"}]}`),
|
||||
Compensate: &flow.ActionRef{
|
||||
Action: "release_inventory",
|
||||
Params: json.RawMessage(`{"lines":[{"reference":"l1","quantity":1,"locationId":"se"}]}`),
|
||||
},
|
||||
}}}
|
||||
if _, err := eng.Run(context.Background(), reserve, flow.NewState(id, nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fake.reserved) != 1 {
|
||||
t.Fatalf("reserved = %d, want 1", len(fake.reserved))
|
||||
}
|
||||
got := fake.reserved[0]
|
||||
if got.HolderID != "order:"+OrderId(id).String() {
|
||||
t.Fatalf("holderId = %q", got.HolderID)
|
||||
}
|
||||
if got.TTL != 10*time.Minute {
|
||||
t.Fatalf("ttl = %s", got.TTL)
|
||||
}
|
||||
if len(got.Lines) != 1 || got.Lines[0].SKU != "ABC" || got.Lines[0].Quantity != 1 {
|
||||
t.Fatalf("lines = %+v", got.Lines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCapturedPredicateGatesStep(t *testing.T) {
|
||||
@@ -94,3 +189,148 @@ func TestAmqpEmitHookPublishes(t *testing.T) {
|
||||
t.Fatalf("expected one message routed to k, got %v", fp.msgs)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeEmailSender struct {
|
||||
defaultFrom mail.Address
|
||||
msgs []EmailMessage
|
||||
}
|
||||
|
||||
func (f *fakeEmailSender) DefaultFrom() mail.Address { return f.defaultFrom }
|
||||
|
||||
func (f *fakeEmailSender) Send(_ context.Context, msg EmailMessage) error {
|
||||
f.msgs = append(f.msgs, msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSendEmailHookRendersFulfillmentTemplate(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
reg := flow.NewRegistry()
|
||||
RegisterFlowActions(reg, pool, NewMockProvider())
|
||||
|
||||
sender := &fakeEmailSender{defaultFrom: mail.Address{Name: "Ops", Address: "ops@example.com"}}
|
||||
templates := newEmailTemplateCatalog(map[string]EmailTemplate{
|
||||
"ship": {
|
||||
Subject: "Order {{ .Order.OrderReference }} shipped",
|
||||
Text: "Tracking {{ .Fulfillment.TrackingNumber }} for {{ .Order.CustomerEmail }}",
|
||||
},
|
||||
})
|
||||
RegisterEmailHook(reg, pool, sender, templates)
|
||||
eng := flow.NewEngine(reg, nil)
|
||||
|
||||
const id = 701
|
||||
st := flow.NewState(id, nil)
|
||||
po := placeMsg()
|
||||
po.CustomerEmail = "alice@example.com"
|
||||
po.CustomerName = "Alice"
|
||||
st.Vars[PlaceOrderVar] = po
|
||||
|
||||
def, err := EmbeddedFlow("place-and-pay")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := eng.Run(context.Background(), def, st); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ship := &flow.Definition{Name: "ship", Steps: []flow.Step{{
|
||||
Name: "fulfill",
|
||||
Action: "create_fulfillment",
|
||||
Params: json.RawMessage(`{"carrier":"postnord","trackingNumber":"TRACK-123","trackingUri":"https://track.example/TRACK-123","lines":[{"reference":"l1","quantity":1}]}`),
|
||||
Hooks: flow.Hooks{After: []flow.HookRef{{Type: "send_email", Params: json.RawMessage(`{"template":"ship"}`)}}},
|
||||
}}}
|
||||
if _, err := eng.Run(context.Background(), ship, flow.NewState(id, nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(sender.msgs) != 1 {
|
||||
t.Fatalf("sent %d emails, want 1", len(sender.msgs))
|
||||
}
|
||||
got := sender.msgs[0]
|
||||
if got.From.Address != "ops@example.com" {
|
||||
t.Fatalf("from = %q, want ops@example.com", got.From.Address)
|
||||
}
|
||||
if len(got.To) != 1 || got.To[0].Address != "alice@example.com" {
|
||||
t.Fatalf("to = %+v, want alice@example.com", got.To)
|
||||
}
|
||||
if got.Subject != "Order ref-1 shipped" {
|
||||
t.Fatalf("subject = %q", got.Subject)
|
||||
}
|
||||
if !strings.Contains(got.TextBody, "TRACK-123") {
|
||||
t.Fatalf("text body = %q, want tracking number", got.TextBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFulfillmentWebhookPostsOrderPayload(t *testing.T) {
|
||||
pool := newPool(t)
|
||||
reg := flow.NewRegistry()
|
||||
RegisterFlowActions(reg, pool, NewMockProvider())
|
||||
|
||||
var gotMethod, gotPath, gotHeader string
|
||||
var gotBody map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
gotPath = r.URL.Path
|
||||
gotHeader = r.Header.Get("X-Integration")
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Fatalf("decode webhook body: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
RegisterFulfillmentWebhookHook(reg, pool, srv.Client())
|
||||
eng := flow.NewEngine(reg, nil)
|
||||
|
||||
const id = 702
|
||||
st := flow.NewState(id, nil)
|
||||
po := placeMsg()
|
||||
po.CustomerEmail = "dropship@example.com"
|
||||
st.Vars[PlaceOrderVar] = po
|
||||
|
||||
def, err := EmbeddedFlow("place-and-pay")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := eng.Run(context.Background(), def, st); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ship := &flow.Definition{Name: "ship", Steps: []flow.Step{{
|
||||
Name: "fulfill",
|
||||
Action: "create_fulfillment",
|
||||
Params: json.RawMessage(`{"carrier":"dropshipper","trackingNumber":"DS-123","lines":[{"reference":"l1","quantity":1}]}`),
|
||||
Hooks: flow.Hooks{After: []flow.HookRef{{
|
||||
Type: "fulfillment_webhook",
|
||||
Params: json.RawMessage(fmt.Sprintf(`{
|
||||
"url": %q,
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"X-Integration": "dropship {{ .Order.OrderReference }}"
|
||||
}
|
||||
}`, srv.URL+`/hooks/{{ .OrderID }}`)),
|
||||
}}},
|
||||
}}}
|
||||
if _, err := eng.Run(context.Background(), ship, flow.NewState(id, nil)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if gotMethod != http.MethodPost {
|
||||
t.Fatalf("method = %q, want POST", gotMethod)
|
||||
}
|
||||
if gotPath != "/hooks/"+OrderId(id).String() {
|
||||
t.Fatalf("path = %q", gotPath)
|
||||
}
|
||||
if gotHeader != "dropship ref-1" {
|
||||
t.Fatalf("header = %q", gotHeader)
|
||||
}
|
||||
if gotBody["orderId"] != OrderId(id).String() {
|
||||
t.Fatalf("body orderId = %v", gotBody["orderId"])
|
||||
}
|
||||
fulfillment, ok := gotBody["fulfillment"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("body fulfillment missing: %#v", gotBody["fulfillment"])
|
||||
}
|
||||
if fulfillment["trackingNumber"] != "DS-123" {
|
||||
t.Fatalf("trackingNumber = %v", fulfillment["trackingNumber"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user