// Package catalogtest holds shared helpers for the platform catalog test // harnesses — cmd/catalog-emit (consumer harness) and cmd/event-emit-publisher // (test publisher) both use these to seed a source-item set with the same // shape conventions. Anything in this package is a test-only utility; do // not import from production code. // // Scope: this package is parked under platform/cmd/internal/ on purpose. // Go's internal-package rule restricts imports to packages within the // parent directory's subtree, so only platform/cmd/* binaries can import // it. That keeps the test-helper API out of the public platform surface // (e.g. platform/event, platform/rabbit must not import catalogtest — // they're cross-module seams, not test scaffolding). package catalogtest import ( "encoding/json" "fmt" "math/rand" "os" "strconv" "git.k6n.net/mats/platform/catalog" ) // LoadOrSynthesize returns one source-batch per tenant name. If itemsPath // is non-empty, it parses a {"tenant":[...]}-keyed JSON map and returns // it unchanged. Otherwise it generates -n synthetic items per tenant // using the tenant's registered Schema's field names, with skuPrefix // prepended to each SKU. // // schemaFor is supplied so callers must already have a Registry built // (production code paths usually have a Registry at composition root; // publishing this helper to test code keeps the hot path clean). // // skuPrefix is a single-character prefix that lets log lines / probe // output attribute synthetic items to the binary that produced them // ("B" for catalog-emit, "P" for publisher). Substituting per-binary // retains recognisability in mixed test runs. // // Determinism: synth items are content-version-identical between runs at // the same (schema, seed, i). UpdatedAt is Unix-ms seeded from `i`. func LoadOrSynthesize(itemsPath string, tenants []string, n int, seed int64, reg *catalog.Registry, skuPrefix string) (map[string][]map[string]any, error) { if itemsPath != "" { data, err := os.ReadFile(itemsPath) if err != nil { return nil, err } var raw map[string][]map[string]any if err := json.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("parse items: %w", err) } return raw, nil } rng := rand.New(rand.NewSource(seed)) out := make(map[string][]map[string]any, len(tenants)) for _, t := range tenants { s := reg.SchemaFor(t) items := make([]map[string]any, n) for i := 0; i < n; i++ { items[i] = SynthItem(s, rng, i, skuPrefix) } out[t] = items } return out, nil } // SynthItem builds one source-map whose key names match the supplied // schema's field names. catalog.Apply on (s, item) succeeds for any // schema that uses the same field-name conventions as DefaultSchema() / // BygglagretSchema() / a Registry-supplied override. // // skuPrefix is prepended to each SKU (e.g. "B" or "P"); reasonable // titles and prices are derived from a stable per-i seed plus the rng. // Title text is intentionally uniform across binaries so log lines // diff-cleanly between catalog-emit and event-emit-publisher runs. // // UpdatedAt is deterministic per i (Unix-ms, stable across runs) so the // synthesised projection is content-version-identical between runs at // the same (schema, seed, i). func SynthItem(s catalog.Schema, rng *rand.Rand, i int, skuPrefix string) map[string]any { sku := fmt.Sprintf("%s%07d", skuPrefix, i) m := map[string]any{} if s.SKUField != "" { m[s.SKUField] = sku } if s.TitleField != "" { m[s.TitleField] = fmt.Sprintf("Catalog Item %07d", i) } if s.PriceField != "" { m[s.PriceField] = int64(99 + rng.Intn(9000)) } if s.CurrencyField != "" { m[s.CurrencyField] = "SEK" } if s.TaxClassField != "" { m[s.TaxClassField] = "standard" } if s.InventoryTrackedF != "" { m[s.InventoryTrackedF] = rng.Intn(2) == 0 } if s.DropShipF != "" { m[s.DropShipF] = rng.Intn(8) == 0 } if s.CategoryField != "" { m[s.CategoryField] = fmt.Sprintf("c-%d", rng.Intn(50)) } if s.BrandField != "" { m[s.BrandField] = "Acme" } if s.UpdatedAtField != "" { m[s.UpdatedAtField] = int64(1700000000000 + i) } return m } // StringField returns the string value of a source key, accepting // strings or integral-number source values (mirrors writeradmin's // helper of the same name). Used by delete-marker detection where // the body field may be either JSON-string "999" or JSON-number 999. // // Empty key returns ""; unknown type returns "". Caller decides // what "" means (in writeradmin / publisher buildBatch it means // "skip this delete — no SKU to delete on"). func StringField(src map[string]any, key string) string { if key == "" { return "" } switch v := src[key].(type) { case string: return v case float64: if v == float64(int64(v)) { return strconv.FormatInt(int64(v), 10) } return strconv.FormatFloat(v, 'f', -1, 64) } return "" }