301 lines
8.7 KiB
Go
301 lines
8.7 KiB
Go
package ucp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
|
"git.k6n.net/mats/go-cart-actor/pkg/order"
|
|
messages "git.k6n.net/mats/go-cart-actor/proto/order"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
// testOrderApplier implements OrderReadApplier with an in-memory map of grains.
|
|
type testOrderApplier struct {
|
|
grains map[uint64]*order.OrderGrain
|
|
}
|
|
|
|
func newTestOrderApplier() *testOrderApplier {
|
|
return &testOrderApplier{
|
|
grains: make(map[uint64]*order.OrderGrain),
|
|
}
|
|
}
|
|
|
|
func (a *testOrderApplier) Get(_ context.Context, id uint64) (*order.OrderGrain, error) {
|
|
g, ok := a.grains[id]
|
|
if !ok {
|
|
// Auto-create so non-existent orders return StatusNew (expected 404).
|
|
g = order.NewOrderGrain(id, time.UnixMilli(1000000))
|
|
a.grains[id] = g
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
func (a *testOrderApplier) Apply(_ context.Context, id uint64, msgs ...proto.Message) (*actor.MutationResult[order.OrderGrain], error) {
|
|
g, err := a.Get(nil, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results := make([]actor.ApplyResult, len(msgs))
|
|
for i, msg := range msgs {
|
|
results[i] = actor.ApplyResult{Type: string(msg.ProtoReflect().Descriptor().FullName())}
|
|
switch m := msg.(type) {
|
|
case *messages.CancelOrder:
|
|
if g.Status != order.StatusPending && g.Status != order.StatusAuthorized {
|
|
results[i].Error = errRejected("cannot cancel in status " + string(g.Status))
|
|
break
|
|
}
|
|
g.Status = order.StatusCancelled
|
|
g.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
|
|
case *messages.CreateFulfillment:
|
|
if g.Status != order.StatusCaptured && g.Status != order.StatusPartiallyFulfilled {
|
|
results[i].Error = errRejected("cannot fulfill in status " + string(g.Status))
|
|
break
|
|
}
|
|
fulfillment := order.Fulfillment{
|
|
ID: m.Id,
|
|
Carrier: m.Carrier,
|
|
TrackingNumber: m.TrackingNumber,
|
|
TrackingURI: m.TrackingUri,
|
|
}
|
|
for _, fl := range m.Lines {
|
|
fulfillment.Lines = append(fulfillment.Lines, order.FulfillmentEntry{
|
|
Reference: fl.Reference,
|
|
Quantity: int(fl.Quantity),
|
|
})
|
|
}
|
|
g.Fulfillments = append(g.Fulfillments, fulfillment)
|
|
g.Status = order.StatusFulfilled
|
|
case *messages.RequestReturn:
|
|
ret := order.Return{
|
|
ID: m.Id,
|
|
Reason: m.Reason,
|
|
}
|
|
for _, fl := range m.Lines {
|
|
ret.Lines = append(ret.Lines, order.FulfillmentEntry{
|
|
Reference: fl.Reference,
|
|
Quantity: int(fl.Quantity),
|
|
})
|
|
}
|
|
g.Returns = append(g.Returns, ret)
|
|
case *messages.IssueRefund:
|
|
g.Refunds = append(g.Refunds, order.Refund{
|
|
Provider: m.Provider,
|
|
Amount: m.Amount,
|
|
Reference: m.Reference,
|
|
})
|
|
g.RefundedAmount += m.Amount
|
|
if g.RefundedAmount >= g.CapturedAmount {
|
|
g.Status = order.StatusRefunded
|
|
}
|
|
}
|
|
}
|
|
return &actor.MutationResult[order.OrderGrain]{
|
|
Result: *g,
|
|
Mutations: results,
|
|
}, nil
|
|
}
|
|
|
|
func errRejected(msg string) *actor.MutationError {
|
|
return &actor.MutationError{Message: msg}
|
|
}
|
|
|
|
// seedOrder places an order in the test applier with a given status.
|
|
func seedOrder(a *testOrderApplier, status order.Status) string {
|
|
id, _ := order.NewOrderId()
|
|
g, _ := a.Get(nil, uint64(id))
|
|
g.Status = status
|
|
g.Currency = "SEK"
|
|
g.TotalAmount = 10000
|
|
g.TotalTax = 2000
|
|
g.Lines = []order.Line{
|
|
{Reference: "test-1", Sku: "test-1", Name: "Test Item", Quantity: 2, UnitPrice: 5000, TaxRate: 2500, TotalAmount: 10000, TotalTax: 2000},
|
|
}
|
|
if status == order.StatusCaptured {
|
|
g.CapturedAmount = 10000
|
|
}
|
|
return id.String()
|
|
}
|
|
|
|
func TestOrderHandler_GetOrder(t *testing.T) {
|
|
a := newTestOrderApplier()
|
|
h := OrderHandler(a)
|
|
oid := seedOrder(a, order.StatusCaptured)
|
|
|
|
req := httptest.NewRequest("GET", "/"+oid, nil)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp OrderResponse
|
|
if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.OrderId != oid {
|
|
t.Fatalf("expected orderId %q, got %q", oid, resp.OrderId)
|
|
}
|
|
if resp.Status != "captured" {
|
|
t.Fatalf("expected status 'captured', got %q", resp.Status)
|
|
}
|
|
if len(resp.Lines) != 1 {
|
|
t.Fatalf("expected 1 line, got %d", len(resp.Lines))
|
|
}
|
|
if resp.Lines[0].Sku != "test-1" {
|
|
t.Fatalf("expected sku 'test-1', got %q", resp.Lines[0].Sku)
|
|
}
|
|
}
|
|
|
|
func TestOrderHandler_GetOrderNotFound(t *testing.T) {
|
|
a := newTestOrderApplier()
|
|
h := OrderHandler(a)
|
|
|
|
// Non-existent order (not seeded) returns 404.
|
|
req := httptest.NewRequest("GET", "/nonexistent", nil)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404 for non-existent order (StatusNew -> 404), got %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestOrderHandler_CancelOrder(t *testing.T) {
|
|
a := newTestOrderApplier()
|
|
h := OrderHandler(a)
|
|
oid := seedOrder(a, order.StatusAuthorized)
|
|
|
|
body := `{"reason": "customer request"}`
|
|
req := httptest.NewRequest("POST", "/"+oid+"/cancel", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp OrderResponse
|
|
json.NewDecoder(rec.Body).Decode(&resp)
|
|
if resp.Status != "cancelled" {
|
|
t.Fatalf("expected status 'cancelled', got %q", resp.Status)
|
|
}
|
|
}
|
|
|
|
func TestOrderHandler_CancelOrder_IllegalState(t *testing.T) {
|
|
a := newTestOrderApplier()
|
|
h := OrderHandler(a)
|
|
oid := seedOrder(a, order.StatusFulfilled) // can't cancel from fulfilled
|
|
|
|
req := httptest.NewRequest("POST", "/"+oid+"/cancel", nil)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("expected 422 for illegal transition, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestOrderHandler_CreateFulfillment(t *testing.T) {
|
|
a := newTestOrderApplier()
|
|
h := OrderHandler(a)
|
|
oid := seedOrder(a, order.StatusCaptured)
|
|
|
|
body := `{"carrier":"PostNord","trackingNumber":"ABC123","lines":[{"reference":"test-1","quantity":2}]}`
|
|
req := httptest.NewRequest("POST", "/"+oid+"/fulfillments", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp OrderResponse
|
|
json.NewDecoder(rec.Body).Decode(&resp)
|
|
if len(resp.Fulfillments) != 1 {
|
|
t.Fatalf("expected 1 fulfillment, got %d", len(resp.Fulfillments))
|
|
}
|
|
if resp.Fulfillments[0].Carrier != "PostNord" {
|
|
t.Fatalf("expected carrier 'PostNord', got %q", resp.Fulfillments[0].Carrier)
|
|
}
|
|
if resp.Fulfillments[0].TrackingNumber != "ABC123" {
|
|
t.Fatalf("expected tracking 'ABC123', got %q", resp.Fulfillments[0].TrackingNumber)
|
|
}
|
|
}
|
|
|
|
func TestOrderHandler_RequestReturn(t *testing.T) {
|
|
a := newTestOrderApplier()
|
|
h := OrderHandler(a)
|
|
oid := seedOrder(a, order.StatusFulfilled)
|
|
|
|
body := `{"reason":"defective","lines":[{"reference":"test-1","quantity":1}]}`
|
|
req := httptest.NewRequest("POST", "/"+oid+"/returns", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp OrderResponse
|
|
json.NewDecoder(rec.Body).Decode(&resp)
|
|
if len(resp.Returns) != 1 {
|
|
t.Fatalf("expected 1 return, got %d", len(resp.Returns))
|
|
}
|
|
if resp.Returns[0].Reason != "defective" {
|
|
t.Fatalf("expected reason 'defective', got %q", resp.Returns[0].Reason)
|
|
}
|
|
}
|
|
|
|
func TestOrderHandler_IssueRefund(t *testing.T) {
|
|
a := newTestOrderApplier()
|
|
h := OrderHandler(a)
|
|
oid := seedOrder(a, order.StatusCaptured)
|
|
|
|
body := `{"amount":3000}`
|
|
req := httptest.NewRequest("POST", "/"+oid+"/refunds", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
var resp OrderResponse
|
|
json.NewDecoder(rec.Body).Decode(&resp)
|
|
if len(resp.Refunds) != 1 {
|
|
t.Fatalf("expected 1 refund, got %d", len(resp.Refunds))
|
|
}
|
|
if resp.Refunds[0].Amount != 3000 {
|
|
t.Fatalf("expected amount 3000, got %d", resp.Refunds[0].Amount)
|
|
}
|
|
}
|
|
|
|
func TestOrderHandler_SignatureIntegration(t *testing.T) {
|
|
cfg := mustLoadTestKey(t)
|
|
a := newTestOrderApplier()
|
|
oid := seedOrder(a, order.StatusCaptured)
|
|
|
|
h := WithSigning(OrderHandler(a), cfg)
|
|
req := httptest.NewRequest("GET", "/"+oid, nil)
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", rec.Code)
|
|
}
|
|
if rec.Header().Get("Signature-Input") == "" {
|
|
t.Fatal("expected Signature-Input with signing enabled")
|
|
}
|
|
}
|