151 lines
5.1 KiB
Go
151 lines
5.1 KiB
Go
// 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.Category2Field != "" {
|
|
m[s.Category2Field] = fmt.Sprintf("c2-%d", rng.Intn(20))
|
|
}
|
|
if s.Category3Field != "" {
|
|
m[s.Category3Field] = fmt.Sprintf("c3-%d", rng.Intn(10))
|
|
}
|
|
if s.Category4Field != "" {
|
|
m[s.Category4Field] = fmt.Sprintf("c4-%d", rng.Intn(5))
|
|
}
|
|
if s.Category5Field != "" {
|
|
m[s.Category5Field] = fmt.Sprintf("c5-%d", rng.Intn(3))
|
|
}
|
|
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 ""
|
|
}
|