all the refactor
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.k6n.net/mats/platform/catalog"
|
||||
"git.k6n.net/mats/platform/money"
|
||||
)
|
||||
|
||||
// TestProjectionCache_ApplyUpserts covers the happy path: a batch of N
|
||||
// projections reaches the cache via Apply, all are visible via Get.
|
||||
func TestProjectionCache_ApplyUpserts(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
updates := []catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{ID: "id-A", SKU: "A", PriceIncVat: money.Cents(100_00), TaxClass: "standard"}},
|
||||
{Projection: catalog.Projection{ID: "id-B", SKU: "B", PriceIncVat: money.Cents(150_00), TaxClass: "reduced"}},
|
||||
{Projection: catalog.Projection{ID: "id-C", SKU: "C", PriceIncVat: money.Cents(0), TaxClass: ""}},
|
||||
}
|
||||
upserts, deletes := c.Apply(updates)
|
||||
if upserts != 3 || deletes != 0 {
|
||||
t.Fatalf("counts: upserts=%d deletes=%d, want 3/0", upserts, deletes)
|
||||
}
|
||||
for _, want := range []struct {
|
||||
sku string
|
||||
price money.Cents
|
||||
taxCls string
|
||||
}{
|
||||
{"A", money.Cents(100_00), "standard"},
|
||||
{"B", money.Cents(150_00), "reduced"},
|
||||
{"C", money.Cents(0), ""},
|
||||
} {
|
||||
got, ok := c.Get(want.sku)
|
||||
if !ok {
|
||||
t.Fatalf("Get(%q): missing", want.sku)
|
||||
}
|
||||
if got.PriceIncVat != want.price || got.TaxClass != want.taxCls {
|
||||
t.Fatalf("Get(%q) = %+v, want price=%d taxCls=%q", want.sku, got, want.price, want.taxCls)
|
||||
}
|
||||
}
|
||||
if c.Len() != 3 {
|
||||
t.Fatalf("Len = %d, want 3", c.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// TestProjectionCache_DeleteTombstone verifies that a delete update sets a
|
||||
// tombstone (not removes the entry), so:
|
||||
// - Get returns (_, false) on a deleted SKU
|
||||
// - IsDeleted returns true
|
||||
// - the same entry continues to count toward Len (size accounting)
|
||||
func TestProjectionCache_DeleteTombstone(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{ID: "id-A", SKU: "A", PriceIncVat: money.Cents(100_00)}},
|
||||
})
|
||||
if _, ok := c.Get("A"); !ok {
|
||||
t.Fatalf("A: expected hit before delete")
|
||||
}
|
||||
|
||||
upserts, deletes := c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{SKU: "A"}, Deleted: true},
|
||||
})
|
||||
if upserts != 0 || deletes != 1 {
|
||||
t.Fatalf("delete counts: upserts=%d deletes=%d, want 0/1", upserts, deletes)
|
||||
}
|
||||
|
||||
if got, ok := c.Get("A"); ok {
|
||||
t.Fatalf("A after delete: got %+v ok=true, want miss", got)
|
||||
}
|
||||
if !c.IsDeleted("A") {
|
||||
t.Fatalf("A after delete: IsDeleted false")
|
||||
}
|
||||
if c.IsDeleted("UNKNOWN") {
|
||||
t.Fatalf("UNKNOWN: IsDeleted should be false (entry doesn't exist)")
|
||||
}
|
||||
if c.Len() != 1 {
|
||||
t.Fatalf("Len after tombstone: %d, want 1 (tombstone still in map)", c.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// TestProjectionCache_BusResurrect verifies that a fresh upsert after a delete
|
||||
// clears the tombstone (live entry replaces it): Get returns the projection,
|
||||
// IsDeleted flips to false. Mirrors how the live catalog keeps flipping state.
|
||||
func TestProjectionCache_BusResurrect(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{SKU: "A", PriceIncVat: money.Cents(50_00)}},
|
||||
{Projection: catalog.Projection{SKU: "A"}, Deleted: true},
|
||||
})
|
||||
if !c.IsDeleted("A") {
|
||||
t.Fatalf("A: not tombstoned after delete")
|
||||
}
|
||||
|
||||
c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{SKU: "A", PriceIncVat: money.Cents(75_00)}},
|
||||
})
|
||||
|
||||
got, ok := c.Get("A")
|
||||
if !ok {
|
||||
t.Fatalf("A: expected live after re-upsert")
|
||||
}
|
||||
if got.PriceIncVat != money.Cents(75_00) {
|
||||
t.Fatalf("A re-upsert price = %d, want 7500", got.PriceIncVat)
|
||||
}
|
||||
if c.IsDeleted("A") {
|
||||
t.Fatalf("A: still tombstoned after re-upsert")
|
||||
}
|
||||
}
|
||||
|
||||
// TestProjectionCache_EmptySKUIgnored validates that an upsert with an empty
|
||||
// SKU is a no-op (defensive — bus should never publish "" but cheaper to log
|
||||
// + drop than to populate an ambiguous cache slot).
|
||||
func TestProjectionCache_EmptySKUIgnored(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
upserts, _ := c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{SKU: "", PriceIncVat: money.Cents(100_00)}},
|
||||
})
|
||||
if upserts != 0 {
|
||||
t.Fatalf("empty-SKU upsert: %d, want 0", upserts)
|
||||
}
|
||||
if c.Len() != 0 {
|
||||
t.Fatalf("Len after empty-SKU: %d, want 0", c.Len())
|
||||
}
|
||||
}
|
||||
|
||||
// TestProjectionCache_BusRaceSafe runs Apply + Get concurrently under -race
|
||||
// to lock down the lock order. Run via `go test -race ./cmd/cart/...`.
|
||||
func TestProjectionCache_BusRaceSafe(t *testing.T) {
|
||||
c := newCatalogProjectionCache()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for i := 0; i < 500; i++ {
|
||||
c.Apply([]catalog.ProjectionUpdate{
|
||||
{Projection: catalog.Projection{SKU: "X", PriceIncVat: money.Cents(int64(i * 100))}},
|
||||
})
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 2000; i++ {
|
||||
_, _ = c.Get("X")
|
||||
_ = c.IsDeleted("X")
|
||||
_ = c.Len()
|
||||
}
|
||||
<-done
|
||||
if _, ok := c.Get("X"); !ok {
|
||||
t.Fatalf("final Get(X): miss after concurrent writes")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTaxResolveBp covers the static TaxClass→bp mapping used by
|
||||
// ApplyProjectionOverlay. Adds a regression net for the common Nordic rates.
|
||||
func TestTaxResolveBp(t *testing.T) {
|
||||
cases := []struct {
|
||||
class string
|
||||
want int
|
||||
}{
|
||||
{"standard", 2500},
|
||||
{"reduced", 1200},
|
||||
{"lowered", 600},
|
||||
{"zero", 0},
|
||||
{"exempt", 0},
|
||||
{"", 2500}, // missing class defaults to standard (matches mutation registry)
|
||||
{"unknown-class", 2500},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := resolveTaxBp(tc.class)
|
||||
if got != tc.want {
|
||||
t.Errorf("resolveTaxBp(%q) = %d, want %d", tc.class, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user