Files
commerce/models/catalogentry/catalogentry_test.go
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

367 lines
12 KiB
Go

package catalogentry
import (
"context"
"testing"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/util/test/ae"
)
func sysDB(c context.Context) *datastore.Datastore {
return SystemDB(c)
}
func TestSeed_IdempotentAndComplete(t *testing.T) {
c := ae.NewContext()
defer c.Close()
db := sysDB(c)
created, err := Seed(db)
if err != nil {
t.Fatalf("seed: %v", err)
}
rows, _ := HanzoSeedRows()
if len(rows) == 0 {
t.Fatal("embedded snapshot is empty")
}
if created != len(rows) {
t.Fatalf("first seed created %d, want %d (all snapshot rows)", created, len(rows))
}
created2, err := Seed(db)
if err != nil {
t.Fatalf("re-seed: %v", err)
}
if created2 != 0 {
t.Fatalf("re-seed created %d, want 0 (idempotent)", created2)
}
}
// TestProject_ConformsToContract checks the projection matches the
// @hanzo/products CatalogEntry contract for known seeded entries.
func TestProject_ConformsToContract(t *testing.T) {
c := ae.NewContext()
defer c.Close()
db := sysDB(c)
if _, err := Seed(db); err != nil {
t.Fatalf("seed: %v", err)
}
cat, err := Project(db, "hanzo")
if err != nil {
t.Fatalf("project: %v", err)
}
if cat.Brand != "hanzo" || len(cat.Categories) != 11 {
t.Fatalf("hanzo: brand=%s cats=%d, want hanzo/10", cat.Brand, len(cat.Categories))
}
if cat.Categories[0].Label != "AI" || cat.Categories[0].ID != "ai" {
t.Fatalf("first category = %+v, want {ai, AI, 0}", cat.Categories[0])
}
byID := map[string]Item{}
for _, p := range cat.Products {
byID[p.ID] = p
if p.IconKey == "" {
t.Fatalf("%s: empty iconKey", p.ID)
}
if p.BrandColor == "" {
t.Fatalf("%s: empty brandColor (must be a swatch key)", p.ID)
}
if p.ID != p.Slug {
t.Fatalf("%s: id != slug (%s)", p.ID, p.Slug)
}
if p.Route == "" {
t.Fatalf("%s: empty route", p.ID)
}
// Only a SERVICE has a route of its own. A client consumes the API and a
// pending product is not fronted by it, so neither may carry an apiPath —
// asserting one on every product is what made working clients read as
// broken, and asserting none is what let dead paths ship as "enabled".
switch p.Kind {
case KindClient, KindPending:
if p.ApiPath != "" {
t.Fatalf("%s is %s yet projects apiPath %q", p.ID, p.Kind, p.ApiPath)
}
case KindService:
if len(p.ApiPath) < 3 || p.ApiPath[:3] != "/v1" {
t.Fatalf("%s: apiPath %q not /v1-prefixed", p.ID, p.ApiPath)
}
default:
t.Fatalf("%s: kind %q is none of %q, %q, %q", p.ID, p.Kind, KindService, KindClient, KindPending)
}
}
m, ok := byID["models"]
if !ok {
t.Fatal("seeded entry 'models' missing from projection")
}
if m.Name != "Models" || m.Category != "AI" || m.IconKey != "Brain" {
t.Fatalf("models = %+v, want Models/AI/Brain", m)
}
if m.BrandColor != "violet" { // swatch KEY, not hex
t.Fatalf("models.brandColor = %q, want swatch key 'violet'", m.BrandColor)
}
if m.Route != "/models" || m.DocsUrl != "https://docs.hanzo.ai/docs/models" {
t.Fatalf("models route/docs = %q / %q", m.Route, m.DocsUrl)
}
// Every capability carries the enriched taxonomy fields.
if m.ApiRoute != "api.hanzo.ai/v1/models" {
t.Fatalf("models.apiRoute = %q, want api.hanzo.ai/v1/models", m.ApiRoute)
}
if m.GithubUrl == "" {
t.Fatalf("models.githubUrl empty")
}
// models has a real public price sourced from the pricing table.
if m.Pricing == nil || m.Pricing.PublicPrice == "" || m.Pricing.PublicPrice == "TODO" {
t.Fatalf("models.pricing missing a real public price: %+v", m.Pricing)
}
// Economics (cost + margin) are an operator's numbers, set in admin or by a
// sync, never shipped in the seed. The public projection could not expose
// them anyway (Item has no Private field), so assert the raw entry.
mach := New(db)
if ok, _ := mach.Query().Filter("Slug=", "machines").Get(); ok {
if mach.Private != nil {
t.Fatalf("seeded machines carries economics: %+v", mach.Private)
}
}
}
// hanzo.ai hydrates its product menu from this projection, and `kind` is the
// only thing in it that says not to render an API link for the CLI. So the wire
// must STATE the kind on every row rather than leave it to be inferred from an
// empty apiPath — and a row that stores none is a service, which is what every
// row written before the field is.
func TestProject_StatesKindAndDefaultsToService(t *testing.T) {
c := ae.NewContext()
defer c.Close()
db := sysDB(c)
if _, err := Seed(db); err != nil {
t.Fatalf("seed: %v", err)
}
cat, err := Project(db, "hanzo")
if err != nil {
t.Fatalf("project: %v", err)
}
clients := 0
for _, p := range cat.Products {
if p.Kind == "" {
t.Fatalf("%s: projection omits kind — a consumer would have to guess it", p.ID)
}
if p.Kind == KindClient {
clients++
}
}
if clients != len(clientProducts) {
t.Fatalf("projected %d clients, want %d", clients, len(clientProducts))
}
// A stored row that predates the field carries no kind and is API-backed.
if got := KindOf(&CatalogEntry{}); got != KindService {
t.Fatalf("KindOf(unset) = %q, want %q", got, KindService)
}
}
// TestProject_CategoryScopedByBrand proves scoping is by CATEGORY (matching
// @hanzo/products catalogForBrand): a sovereign brand (lux) sees only entries in
// its 4 categories, never an AI/Compute/etc. entry.
func TestProject_CategoryScopedByBrand(t *testing.T) {
c := ae.NewContext()
defer c.Close()
db := sysDB(c)
if _, err := Seed(db); err != nil {
t.Fatalf("seed: %v", err)
}
lux, err := Project(db, "lux")
if err != nil {
t.Fatalf("project lux: %v", err)
}
if len(lux.Categories) != 4 {
t.Fatalf("lux categories = %d, want 4", len(lux.Categories))
}
allowed := map[string]bool{"Web3": true, "Network": true, "Security": true, "Dev": true}
for _, p := range lux.Products {
if !allowed[p.Category] {
t.Fatalf("lux projection leaked %q in non-lux category %q", p.ID, p.Category)
}
if p.ID == "models" {
t.Fatal("lux projection contains an AI-category entry (models)")
}
}
hanzo, _ := Project(db, "hanzo")
if len(hanzo.Products) <= len(lux.Products) {
t.Fatalf("hanzo products (%d) should exceed lux (%d)", len(hanzo.Products), len(lux.Products))
}
}
// asFloat coerces a JSON-decoded Metadata value (numbers round-trip as float64
// through the SQLite Metadata_ blob) to float64 for comparison.
func asFloat(t *testing.T, v interface{}) float64 {
t.Helper()
f, ok := v.(float64)
if !ok {
t.Fatalf("metadata value %#v is not a number", v)
}
return f
}
// TestSeedInfra_IdempotentAndComplete proves the infra-tier seed creates exactly
// the embedded rows (11 cloud + 3 gpu + 3 datastore), is idempotent, and that the
// count-gated SeedInfraIfEmpty seeds once then no-ops.
func TestSeedInfra_IdempotentAndComplete(t *testing.T) {
c := ae.NewContext()
defer c.Close()
db := sysDB(c)
rows, err := InfraSeedRows()
if err != nil {
t.Fatalf("infra seed rows: %v", err)
}
if len(rows) != 17 {
t.Fatalf("embedded infra snapshot has %d rows, want 17 (11 cloud + 3 gpu + 3 datastore)", len(rows))
}
// The gate seeds every row when the infra scope is empty (this test's db
// starts empty), independently of the hanzo product snapshot.
created, err := SeedInfraIfEmpty(db)
if err != nil {
t.Fatalf("seed infra: %v", err)
}
if created != len(rows) {
t.Fatalf("first infra seed created %d, want %d", created, len(rows))
}
// Idempotent: the gate no-ops once the tiers exist, and so does the
// per-row seed (never clobbers, never duplicates).
if n, err := SeedInfraIfEmpty(db); err != nil || n != 0 {
t.Fatalf("SeedInfraIfEmpty on populated db = (%d, %v), want (0, nil)", n, err)
}
if n, err := SeedInfra(db); err != nil || n != 0 {
t.Fatalf("SeedInfra re-run = (%d, %v), want (0, nil) (idempotent)", n, err)
}
}
// TestSeedInfra_ProjectionAndValues proves the infra tiers project under
// ?brand=infra with their category, PriceCents, and structured Metadata intact —
// and that they never leak into a per-brand (hanzo) catalog.
func TestSeedInfra_ProjectionAndValues(t *testing.T) {
c := ae.NewContext()
defer c.Close()
db := sysDB(c)
if _, err := SeedInfra(db); err != nil {
t.Fatalf("seed infra: %v", err)
}
cat, err := Project(db, "infra")
if err != nil {
t.Fatalf("project infra: %v", err)
}
if len(cat.Categories) != 3 {
t.Fatalf("infra categories = %d, want 3", len(cat.Categories))
}
if len(cat.Products) != 17 {
t.Fatalf("infra products = %d, want 17", len(cat.Products))
}
bySlug := map[string]Item{}
counts := map[string]int{}
for _, p := range cat.Products {
bySlug[p.Slug] = p
counts[p.Category]++
if p.PriceCents <= 0 {
t.Fatalf("%s: PriceCents must be a positive display price, got %d", p.Slug, p.PriceCents)
}
if p.Metadata == nil {
t.Fatalf("%s: Metadata must project the structured spec, got nil", p.Slug)
}
}
if counts["cloud"] != 11 || counts["gpu"] != 3 || counts["datastore"] != 3 {
t.Fatalf("category counts = %v, want cloud:11 gpu:3 datastore:3", counts)
}
// Cloud: display price = monthly * 100; Metadata carries the VM spec verbatim.
starter := bySlug["cloud-starter"]
if starter.Category != "cloud" || starter.PriceCents != 500 {
t.Fatalf("cloud-starter = {cat:%s cents:%d}, want {cloud 500}", starter.Category, starter.PriceCents)
}
if asFloat(t, starter.Metadata["priceMonthly"]) != 5 || asFloat(t, starter.Metadata["vcpus"]) != 1 ||
asFloat(t, starter.Metadata["maxVMs"]) != 1 || starter.Metadata["cpuType"] != "shared" {
t.Fatalf("cloud-starter metadata spec wrong: %#v", starter.Metadata)
}
if starter.Metadata["freeTier"] != true {
t.Fatalf("cloud-starter should carry freeTier=true, got %#v", starter.Metadata["freeTier"])
}
// GPU: display price = hourly * 100; Metadata carries gpu/vram/price.
gpu := bySlug["gpu-standard"]
if gpu.Category != "gpu" || gpu.PriceCents != 348 {
t.Fatalf("gpu-standard = {cat:%s cents:%d}, want {gpu 348}", gpu.Category, gpu.PriceCents)
}
if gpu.Metadata["gpu"] != "1x H100" || gpu.Metadata["vram"] != "80 GB" || asFloat(t, gpu.Metadata["price"]) != 3.48 {
t.Fatalf("gpu-standard metadata wrong: %#v", gpu.Metadata)
}
// Datastore: display price = monthly * 100; Metadata carries the tier spec
// AND the shared usage rates.
ds := bySlug["datastore-basic"]
if ds.Category != "datastore" || ds.PriceCents != 6652 {
t.Fatalf("datastore-basic = {cat:%s cents:%d}, want {datastore 6652}", ds.Category, ds.PriceCents)
}
if asFloat(t, ds.Metadata["replicas"]) != 1 || asFloat(t, ds.Metadata["priceHourly"]) != 0.0922 {
t.Fatalf("datastore-basic spec wrong: %#v", ds.Metadata)
}
usage, ok := ds.Metadata["usage"].(map[string]interface{})
if !ok {
t.Fatalf("datastore-basic must carry the usage block, got %#v", ds.Metadata["usage"])
}
storage, ok := usage["storage"].(map[string]interface{})
if !ok || asFloat(t, storage["pricePerGBMonth"]) != 0.0247 {
t.Fatalf("datastore usage.storage rate wrong: %#v", usage["storage"])
}
// Isolation: the infra tiers must NOT leak into the per-brand hanzo catalog.
hanzo, err := Project(db, "hanzo")
if err != nil {
t.Fatalf("project hanzo: %v", err)
}
if len(hanzo.Categories) != 11 {
t.Fatalf("hanzo categories = %d, want 11 (infra scope must not widen it)", len(hanzo.Categories))
}
for _, p := range hanzo.Products {
switch p.Category {
case "cloud", "gpu", "datastore":
t.Fatalf("infra tier %q leaked into the hanzo catalog under %q", p.Slug, p.Category)
}
}
}
func TestProject_UnpublishedExcluded(t *testing.T) {
c := ae.NewContext()
defer c.Close()
db := sysDB(c)
e := New(db)
e.Slug = "hidden"
e.Name = "Hidden"
e.Category = "AI"
e.IconKey = "Lock"
e.BrandColor = "slate"
e.Published = false
if err := e.Create(); err != nil {
t.Fatalf("create: %v", err)
}
cat, err := Project(db, "hanzo")
if err != nil {
t.Fatalf("project: %v", err)
}
for _, p := range cat.Products {
if p.Slug == "hidden" {
t.Fatal("unpublished entry appeared in the public projection")
}
}
}