Files
mats 9f1eca07e9
Build and Publish / BuildAndDeployAmd64 (push) Failing after 5s
Build and Publish / BuildAndDeployArm64 (push) Failing after 7s
parent item mutations
2026-07-08 11:39:42 +02:00

207 lines
6.8 KiB
Go

package main
import (
"encoding/json"
"net/http/httptest"
"testing"
"git.k6n.net/mats/go-cart-actor/pkg/actor"
"git.k6n.net/mats/go-cart-actor/pkg/cart"
)
// TestWriteResult_CartGrainCarriesEvaluatedItems verifies that the per-line
// promotion breakdown lives on CartGrain.EvaluatedItems (so the canonical
// promotion pipeline populates it once per mutation, not at every read
// path) and naturally serialises through WriteResult alongside the rest
// of the grain's tagged fields. There is no envelope wrapper anymore —
// the grain's own JSON tag carries the field. See pkg/cart/evaluated_item.go
// for EvaluatedItem semantics and pkg/cart/cart-grain.go for the EvaluatedItems
// field declaration.
func TestWriteResult_CartGrainCarriesEvaluatedItems(t *testing.T) {
grain := &cart.CartGrain{
Id: 1,
Currency: "SEK",
Items: []*cart.CartItem{
{
Sku: "promo-1",
Quantity: 2,
Price: *cart.NewPriceFromIncVat(10000, 2500),
Discount: cart.NewPriceFromIncVat(2000, 500),
Meta: &cart.ItemMeta{Name: "Promo Item"},
},
},
TotalPrice: cart.NewPriceFromIncVat(20000, 5000),
TotalDiscount: cart.NewPriceFromIncVat(4000, 1000),
EvaluatedItems: []cart.EvaluatedItem{{
Sku: "promo-1",
Name: "Promo Item",
Quantity: 2,
PriceIncVat: 10000,
OrgPriceIncVat: 12000,
DiscountIncVat: 2000,
EffectivePriceIncVat: 9000,
EffectiveTotalIncVat: 18000,
}},
}
s := &PoolServer{}
w := httptest.NewRecorder()
if err := s.WriteResult(w, grain); err != nil {
t.Fatalf("WriteResult: %v", err)
}
var resp map[string]json.RawMessage
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v (body=%q)", err, w.Body.String())
}
for _, key := range []string{"id", "currency", "items", "totalPrice", "totalDiscount", "evaluatedItems"} {
if _, ok := resp[key]; !ok {
t.Fatalf("expected top-level %q in grain serialisation, got keys %v", key, mapKeys(resp))
}
}
var ev []struct {
Sku string `json:"sku"`
DiscountIncVat int64 `json:"discountIncVat"`
EffectiveTotalIncVat int64 `json:"effectiveTotalIncVat"`
EffectivePriceIncVat int64 `json:"effectivePriceIncVat"`
}
if err := json.Unmarshal(resp["evaluatedItems"], &ev); err != nil {
t.Fatalf("evaluatedItems parse: %v", err)
}
if len(ev) != 1 || ev[0].Sku != "promo-1" {
t.Fatalf("expected one promo-1 evaluated item, got %+v", ev)
}
if ev[0].DiscountIncVat != 2000 || ev[0].EffectiveTotalIncVat != 18000 || ev[0].EffectivePriceIncVat != 9000 {
t.Fatalf("evaluatedItem math off: %+v", ev[0])
}
}
// TestWriteResult_MutationResultNestsEvaluatedItems verifies the natural
// post-mutation wire shape: actor.MutationResult[cart.CartGrain] serialises
// Result (the grain) and Mutations (applied-change metadata) at the existing
// top-level positions, and CartGrain's EvaluatedItems rides inside Result
// because the grain owns the field. Previously the envelope wrapper pulled
// evaluatedItems out as a sibling of {result, mutations}; the natural
// nesting is what the user wanted ("no strange patterns on a global level").
// Internal/admin tools that read response.evaluatedItems will need to read
// response.result.evaluatedItems instead. Wrap-around breakage acknowledged.
func TestWriteResult_MutationResultNestsEvaluatedItems(t *testing.T) {
grain := &cart.CartGrain{
Id: 1,
Currency: "SEK",
Items: []*cart.CartItem{
{
Sku: "promo-1",
Quantity: 1,
Price: *cart.NewPriceFromIncVat(10000, 2500),
Discount: cart.NewPriceFromIncVat(3000, 750),
},
},
EvaluatedItems: []cart.EvaluatedItem{{
Sku: "promo-1",
Quantity: 1,
PriceIncVat: 10000,
DiscountIncVat: 3000,
EffectiveTotalIncVat: 7000,
EffectivePriceIncVat: 7000,
}},
}
mr := &actor.MutationResult[cart.CartGrain]{
Result: *grain,
Mutations: []actor.ApplyResult{{Type: "cart.AddItem"}},
}
s := &PoolServer{}
w := httptest.NewRecorder()
if err := s.WriteResult(w, mr); err != nil {
t.Fatalf("WriteResult: %v", err)
}
var resp map[string]json.RawMessage
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
// {result, mutations} are the existing envelope — preserved.
for _, key := range []string{"result", "mutations"} {
if _, ok := resp[key]; !ok {
t.Fatalf("expected top-level %q in mutation response, got %v", key, mapKeys(resp))
}
}
// evaluatedItems no longer rises to the top level (the envelope is
// gone). Confirm absence at the top — it's now nested under result.
if _, hasTop := resp["evaluatedItems"]; hasTop {
t.Fatalf("evaluatedItems leaked to top level — envelope wrapper returned somehow")
}
// Result nests the grain, including EvaluatedItems naturally.
var inner map[string]json.RawMessage
if err := json.Unmarshal(resp["result"], &inner); err != nil {
t.Fatalf("result parse: %v", err)
}
if _, ok := inner["items"]; !ok {
t.Fatalf("result.items missing — Result did not carry the grain")
}
evRaw, ok := inner["evaluatedItems"]
if !ok {
t.Fatalf("result.evaluatedItems missing — grain field should nest under result")
}
var ev []struct {
Sku string `json:"sku"`
DiscountIncVat int64 `json:"discountIncVat"`
}
if err := json.Unmarshal(evRaw, &ev); err != nil {
t.Fatalf("result.evaluatedItems parse: %v", err)
}
if len(ev) != 1 || ev[0].Sku != "promo-1" || ev[0].DiscountIncVat != 3000 {
t.Fatalf("result.evaluatedItems wrong: %+v", ev)
}
// Mutations are still a sibling of result.
var muts []map[string]any
if err := json.Unmarshal(resp["mutations"], &muts); err != nil {
t.Fatalf("mutations parse: %v", err)
}
if len(muts) != 1 || muts[0]["type"] != "cart.AddItem" {
t.Fatalf("mutations wrong: %+v", muts)
}
}
// TestWriteResult_NoEvaluatedItemsOmitsField verifies the EvaluatedItems
// omitempty on CartGrain drops the field entirely when the canonical
// pipeline hasn't run (synthetic grain in a fixture, fresh spawn before
// any promotion-aware mutation). Clients reading evaluatedItems can
// rely on absence to mean "no promotion compute yet".
func TestWriteResult_NoEvaluatedItemsOmitsField(t *testing.T) {
grain := &cart.CartGrain{
Id: 1,
Currency: "SEK",
}
s := &PoolServer{}
w := httptest.NewRecorder()
if err := s.WriteResult(w, grain); err != nil {
t.Fatalf("WriteResult: %v", err)
}
var resp map[string]json.RawMessage
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if _, has := resp["evaluatedItems"]; has {
t.Fatalf("expected evaluatedItems omitted on a grain with nil breakdown")
}
}
// mapKeys returns the sorted set of top-level keys for nicer failure output.
func mapKeys(m map[string]json.RawMessage) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}