package main import ( "testing" messages "git.k6n.net/mats/go-cart-actor/proto/cart" "git.k6n.net/mats/platform/catalog" "git.k6n.net/mats/platform/money" ) // TestApplyProjectionOverlay_PricePositiveOnly covers the contract that a cache // hit with a positive PriceIncVat overrides HTTP-fetched price, but a zero // (unbroadcast) PriceIncVat must NOT overwrite a valid HTTP price — it would // be a silent corruption. func TestApplyProjectionOverlay_PricePositiveOnly(t *testing.T) { startPrice := int64(95_00) base := func() *messages.AddItem { return &messages.AddItem{Sku: "X", Price: startPrice, Name: "http-name", Image: "http.jpg", Tax: 2500} } // Positive cache value wins. m := base() ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", Title: "cache-name", Image: "cache.jpg", PriceIncVat: money.Cents(120_00), TaxClass: "reduced"}) if m.Price != 120_00 { t.Errorf("positive cache price: got %d, want 12000", m.Price) } // Zero cache value must NOT overwrite (treat as "no value"). m = base() ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", PriceIncVat: money.Cents(0)}) if m.Price != startPrice { t.Errorf("zero cache price overwrote HTTP price: got %d, want %d (HTTP)", m.Price, startPrice) } } // TestApplyProjectionOverlay_TaxOnlyWhenClassSet verifies the TaxClass-skip on // an unclassified cache entry: HTTP rate is more trustworthy than a guessed // 2500 default, so msg.Tax stays untouched. func TestApplyProjectionOverlay_TaxOnlyWhenClassSet(t *testing.T) { base := func() *messages.AddItem { return &messages.AddItem{Sku: "X", Tax: 600} // HTTP-fetched lowered 6% } // Empty TaxClass: HTTP rate preserved. m := base() ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", TaxClass: ""}) if m.Tax != 600 { t.Errorf("empty TaxClass overwrote HTTP tax: got %d, want 600", m.Tax) } // Non-empty: cache rate wins. m = base() ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", TaxClass: "reduced"}) if m.Tax != 1200 { t.Errorf("TaxClass=reduced: got %d, want 1200", m.Tax) } m = base() ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", TaxClass: "zero"}) if m.Tax != 0 { t.Errorf("TaxClass=zero: got %d, want 0", m.Tax) } } // TestApplyProjectionOverlay_DisplayFields verifies Title/Image override only // when the cache actually carries a value (an unbus-fed "" isn't propagated // over a valid HTTP value). func TestApplyProjectionOverlay_DisplayFields(t *testing.T) { base := func() *messages.AddItem { return &messages.AddItem{Sku: "X", Name: "http-name", Image: "http.jpg"} } m := base() ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", Title: "cache-name", Image: "cache.jpg"}) if m.Name != "cache-name" { t.Errorf("Title overlay: got %q, want %q", m.Name, "cache-name") } if m.Image != "cache.jpg" { t.Errorf("Image overlay: got %q, want %q", m.Image, "cache.jpg") } // Empty cache Title/Image must NOT overwrite. m = base() ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", Title: "", Image: ""}) if m.Name != "http-name" { t.Errorf("empty cache Title wiped HTTP name: got %q", m.Name) } if m.Image != "http.jpg" { t.Errorf("empty cache Image wiped HTTP image: got %q", m.Image) } } // TestApplyProjectionOverlay_FlagFlipToTrueOnly locks down the // InventoryTracked / DropShip positive-flip semantics. False in the cache // is the proto zero value and may be unset on the wire; we don't second-guess. func TestApplyProjectionOverlay_FlagFlipToTrueOnly(t *testing.T) { // HTTP set both true (e.g. live SKU). Cache also has them true. Should // stay true. m := &messages.AddItem{InventoryTracked: true, DropShip: true} ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", InventoryTracked: true, DropShip: true}) if !m.InventoryTracked || !m.DropShip { t.Fatalf("true-from-cache over a true-from-HTTP: %+v", m) } // HTTP set false (default), cache says true → upgrade to true. m = &messages.AddItem{InventoryTracked: false, DropShip: false} ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", InventoryTracked: true, DropShip: true}) if !m.InventoryTracked || !m.DropShip { t.Errorf("true-from-cache should flip false-from-HTTP: %+v", m) } // HTTP already true, cache says false → must NOT downgrade (false would // be the proto zero value; trust the live HTTP signal here). m = &messages.AddItem{InventoryTracked: true, DropShip: true} ApplyProjectionOverlay(m, catalog.Projection{SKU: "X", InventoryTracked: false, DropShip: false}) if !m.InventoryTracked || !m.DropShip { t.Errorf("false-from-cache must NOT overwrite true-from-HTTP: %+v", m) } } // TestApplyProjectionOverlay_NilMsgSafe confirms the helper tolerates a nil // pointer (defensive — caller already validates, but a future caller may not). func TestApplyProjectionOverlay_NilMsgSafe(t *testing.T) { // Must not panic. ApplyProjectionOverlay(nil, catalog.Projection{SKU: "X"}) } // TestIsDeletedVsGet asserts the two methods don't conflict: a tombstoned SKU // is reported by both IsDeleted (true) and Get (miss). func TestIsDeletedVsGet(t *testing.T) { c := newCatalogProjectionCache() c.Apply([]catalog.ProjectionUpdate{ {Projection: catalog.Projection{SKU: "DL", PriceIncVat: money.Cents(10_00)}}, {Projection: catalog.Projection{SKU: "DL"}, Deleted: true}, }) if _, ok := c.Get("DL"); ok { t.Fatalf("Get on tombstone: ok=true") } if !c.IsDeleted("DL") { t.Fatalf("IsDeleted on tombstone: false") } if c.IsDeleted("DL-fresh") { t.Fatalf("IsDeleted on absent entry: true") } if _, ok := c.Get("DL-fresh"); ok { t.Fatalf("Get on absent entry: ok=true") } }