package catalog import ( "encoding/json" "testing" ) // TestSchemaRoundTrip verifies a Schema can be JSON-encoded and decoded // without losing operator-tunable fields. Round-trip is the contract // LoadFile relies on. func TestSchemaRoundTrip(t *testing.T) { original := BygglagretSchema() original.TrueValues = []string{"true", "ja"} // tenant-specific raw, err := json.Marshal(original) if err != nil { t.Fatalf("marshal: %v", err) } var decoded Schema if err := json.Unmarshal(raw, &decoded); err != nil { t.Fatalf("unmarshal: %v", err) } if decoded.SKUField != original.SKUField { t.Errorf("SKUField: got %q want %q", decoded.SKUField, original.SKUField) } if decoded.PriceScale != original.PriceScale { t.Errorf("PriceScale: got %d want %d", decoded.PriceScale, original.PriceScale) } if decoded.UpdatedAtSource != original.UpdatedAtSource { t.Errorf("UpdatedAtSource: got %v want %v", decoded.UpdatedAtSource, original.UpdatedAtSource) } if decoded.UpdatedAtField != original.UpdatedAtField { t.Errorf("UpdatedAtField: got %q want %q", decoded.UpdatedAtField, original.UpdatedAtField) } if len(decoded.TrueValues) != 2 || decoded.TrueValues[1] != "ja" { t.Errorf("TrueValues lost on round-trip: %v", decoded.TrueValues) } } // TestDefaultSchemaConsensusShape pins the field-name contract for the // zero-config tenant (the operator-facing shape). func TestDefaultSchemaConsensusShape(t *testing.T) { s := DefaultSchema() if s.Tenant != "default" { t.Errorf("DefaultSchema().Tenant = %q, want %q", s.Tenant, "default") } if s.SKUField != "sku" || s.PriceField != "price_inc_vat" { t.Errorf("consensus shape rotated: SKUField=%q PriceField=%q", s.SKUField, s.PriceField) } if s.PriceScale != 1 || s.IDPrefix != "sku:" { t.Errorf("consensus defaults wrong: PriceScale=%d IDPrefix=%q", s.PriceScale, s.IDPrefix) } if s.UpdatedAtSource != UseSourceField { t.Errorf("consensus updated_at_source = %v, want UseSourceField "+ "(foot-gun 3: stamp-now'ing on rebuild invalidates every consumer cache)", s.UpdatedAtSource) } if len(s.TrueValues) == 0 { t.Errorf("Truth set should not be empty in consensus") } } // TestTrueValueCaseInsensitive pins the lower-cased match used by boolOf. func TestTrueValueCaseInsensitive(t *testing.T) { truthy := DefaultTrueValues() for _, s := range []string{"TRUE", "Yes", "ON", "1"} { // Re-implement the matching here to avoid coupling to builder.go's // internals, but flag if it ever drifts. low := "" for _, c := range s { if c >= 'A' && c <= 'Z' { low += string(c + 32) } else { low += string(c) } } found := false for _, t := range truthy { if low == t { found = true break } } if !found { t.Errorf("truthy set is missing lower-cased %q", s) } } } // TestSchemaZeroValueInvalid documents that the zero value of Schema is // explicitly NOT a usable schema — Apply validates and refuses it. func TestSchemaZeroValueInvalid(t *testing.T) { _, err := Apply(Schema{}, map[string]any{"sku": "ABC", "price_inc_vat": 1}) if err == nil { t.Errorf("zero Schema should be rejected by Apply") } }