platform
This commit is contained in:
1 parent
e706f7d907
commit
21bcb63440
42 files changed
+5903
No files matched your search
@@ -0,0 +1,162 @@
|
||||
package catalog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// json.Number from a json.Decoder running with UseNumber() is the
|
||||
// canonical path for precision-preserving producer parsing. Without this
|
||||
// case, BuildFromMap would log+skip every projection that came through
|
||||
// UseNumber — silently losing items.
|
||||
func TestBuilderJSONNumber(t *testing.T) {
|
||||
// json.Number for SKU
|
||||
src := map[string]any{
|
||||
"sku": json.Number("123456789012345"), // float64-unsafe
|
||||
"title": "BigNumber SKU",
|
||||
"price_inc_vat": json.Number("14995"), // already øre
|
||||
"currency": "SEK",
|
||||
"tax_class": "standard",
|
||||
"updated_at": json.Number("1700000000000"),
|
||||
}
|
||||
p, err := Apply(DefaultSchema(), src)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Number source rejected: %v", err)
|
||||
}
|
||||
if p.SKU != "123456789012345" {
|
||||
t.Errorf("SKU: got %q want %q", p.SKU, "123456789012345")
|
||||
}
|
||||
if p.PriceIncVat != 14995 {
|
||||
t.Errorf("PriceIncVat: got %d want %d", p.PriceIncVat, 14995)
|
||||
}
|
||||
if p.UpdatedAt != 1700000000000 {
|
||||
t.Errorf("UpdatedAt: got %d want %d", p.UpdatedAt, int64(1700000000000))
|
||||
}
|
||||
}
|
||||
|
||||
// Updated=UseSourceField: missing source updated_at MUST be a loud error,
|
||||
// not a stamp-now fallback. (Foot-gun 3 resolution: silence here invalidates
|
||||
// every consumer's cache on rebuild.)
|
||||
func TestBuilderRequiresUpdatedAt(t *testing.T) {
|
||||
src := map[string]any{
|
||||
"sku": "X-1",
|
||||
"price_inc_vat": int64(100),
|
||||
// no "updated_at" key intentionally
|
||||
}
|
||||
_, err := Apply(DefaultSchema(), src)
|
||||
if err == nil {
|
||||
t.Fatal("UseSourceField + missing updated_at should error, not stamp-now")
|
||||
}
|
||||
if !errors.Is(err, ErrProjectionBuild) {
|
||||
t.Errorf("got %v want ErrProjectionBuild", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "updated_at") {
|
||||
t.Errorf("error should mention updated_at: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Same projection under StampAtPublish succeeds even without updated_at —
|
||||
// that's the deliberate-fallback path. Used by sources with no other stamp.
|
||||
// Bind the stamp to real wall-clock time (before ≤ actual ≤ after+slack)
|
||||
// so the test distinguishes "stamp-now was called" from any other non-zero
|
||||
// path.
|
||||
func TestBuilderStampAtPublishFallback(t *testing.T) {
|
||||
s := DefaultSchema()
|
||||
s.UpdatedAtSource = StampAtPublish
|
||||
s.UpdatedAtField = "" // ignored when source is StampAtPublish
|
||||
before := time.Now().UnixMilli()
|
||||
src := map[string]any{
|
||||
"sku": "X-2",
|
||||
"price_inc_vat": int64(100),
|
||||
}
|
||||
p, err := Apply(s, src)
|
||||
if err != nil {
|
||||
t.Fatalf("StampAtPublish should accept missing updated_at: %v", err)
|
||||
}
|
||||
after := time.Now().UnixMilli() + 1000 // generous slack for slow CI
|
||||
if p.UpdatedAt < before || p.UpdatedAt > after {
|
||||
t.Errorf("UpdatedAt should be stamp-now Unix-ms in [%d,%d], got %d", before, after, p.UpdatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
// Fractional float price with scale=1 must be loud-fail — the float arm of
|
||||
// coercePrice has the same foot-gun 4 risk as the string arm (silent 100x
|
||||
// bias if a tenant forgets PriceScale=100 and ships a float literal).
|
||||
func TestBuilderFractionalFloatLowScaleRejected(t *testing.T) {
|
||||
src := map[string]any{
|
||||
"sku": "F-1",
|
||||
"price_inc_vat": 99.95, // float major-units at scale=1
|
||||
"updated_at": int64(1700000000000),
|
||||
}
|
||||
_, err := Apply(DefaultSchema(), src)
|
||||
if err == nil {
|
||||
t.Fatal("fractional float price at scale=1 should error, not silently produce 100 øre")
|
||||
}
|
||||
if !errors.Is(err, ErrProjectionBuild) {
|
||||
t.Errorf("got %v want ErrProjectionBuild", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "PriceScale=100") {
|
||||
t.Errorf("error message should mention PriceScale=100 hint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// json.Number("149.95") at scale=1 must reach the fractional guard via
|
||||
// the json.Number branch — the precision-safe producer route.
|
||||
func TestBuilderFractionalJSONNumberLowScaleRejected(t *testing.T) {
|
||||
src := map[string]any{
|
||||
"sku": "JN-1",
|
||||
"price_inc_vat": json.Number("149.95"),
|
||||
"updated_at": int64(1700000000000),
|
||||
}
|
||||
_, err := Apply(DefaultSchema(), src)
|
||||
if err == nil {
|
||||
t.Fatal("fractional json.Number at scale=1 should error, not silently produce 150 øre")
|
||||
}
|
||||
if !errors.Is(err, ErrProjectionBuild) {
|
||||
t.Errorf("got %v want ErrProjectionBuild", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "PriceScale=100") {
|
||||
t.Errorf("error message should mention PriceScale=100 hint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Fractional string price with scale=1 must be loud-fail (a misconfigured
|
||||
// tenant shipping major units without PriceScale=100 — silent 100x bias).
|
||||
func TestBuilderFractionalStringPriceLowScaleRejected(t *testing.T) {
|
||||
src := map[string]any{
|
||||
"sku": "X-1",
|
||||
"price_inc_vat": "149.95", // major units FRACTION at scale=1
|
||||
"updated_at": int64(1700000000000),
|
||||
}
|
||||
_, err := Apply(DefaultSchema(), src)
|
||||
if err == nil {
|
||||
t.Fatal("fractional string at scale=1 should error, not silently produce 150 øre")
|
||||
}
|
||||
if !errors.Is(err, ErrProjectionBuild) {
|
||||
t.Errorf("got %v want ErrProjectionBuild", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "PriceScale=100") {
|
||||
t.Errorf("error message should mention PriceScale=100 hint: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Same string with scale=100 (correct major-unit config) succeeds.
|
||||
func TestBuilderFractionalStringPriceCorrectScale(t *testing.T) {
|
||||
s := DefaultSchema()
|
||||
s.PriceScale = 100
|
||||
src := map[string]any{
|
||||
"sku": "X-2",
|
||||
"price_inc_vat": "149.95",
|
||||
"updated_at": int64(1700000000000),
|
||||
}
|
||||
p, err := Apply(s, src)
|
||||
if err != nil {
|
||||
t.Fatalf("scale=100 string price rejected: %v", err)
|
||||
}
|
||||
if p.PriceIncVat != 14995 {
|
||||
t.Errorf("PriceIncVat: got %d want %d", p.PriceIncVat, 14995)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user