package plans import ( "errors" "os" "strings" "testing" ) const examplePath = "testdata/example-catalog.json" func exampleCatalog(t *testing.T) *Catalog { t.Helper() c, err := LoadCatalog(examplePath) if err != nil { t.Fatalf("load example catalog: %v", err) } return c } func exampleBytes(t *testing.T) string { t.Helper() b, err := os.ReadFile(examplePath) if err != nil { t.Fatal(err) } return string(b) } func TestParseExampleCatalog(t *testing.T) { c := exampleCatalog(t) if got := c.Plans["free"].Limits.RunningThings; got != 3 { t.Fatalf("free running_things = %d, want 3", got) } if c.DefaultPlan != "free" || c.FreeAccountsPerPerson != 1 { t.Fatalf("default_plan %q, free_accounts_per_person %d", c.DefaultPlan, c.FreeAccountsPerPerson) } } func TestParseCatalogRefuses(t *testing.T) { base := exampleBytes(t) cases := map[string]string{ "empty version": strings.Replace(base, `"version": "2026-10-20-v1"`, `"version": ""`, 1), "default plan missing": strings.Replace(base, `"default_plan": "free"`, `"default_plan": "gold"`, 1), "pack offered not defined": strings.Replace(base, `["apps", "storage", "email", "bandwidth"]`, `["apps", "seats"]`, 1), "pack adds idle": strings.Replace(base, `{"running_things": 5}`, `{"running_things": 5, "idle_seconds": 60}`, 1), "pack adds memory tier": strings.Replace(base, `{"running_things": 5}`, `{"memory_tier": "default"}`, 1), "pack adds a bool": strings.Replace(base, `{"running_things": 5}`, `{"custom_domains": true}`, 1), "max quantity zero": strings.Replace(base, `{"running_things": 5}, "max_quantity": 20`, `{"running_things": 5}, "max_quantity": 0`, 1), "unknown memory tier": strings.Replace(base, `"memory_tier": "small"`, `"memory_tier": "tiny"`, 1), "unknown field": strings.Replace(base, `"default_plan": "free",`, `"default_plan": "free", "surprise": 1,`, 1), "no plans": `{"version": "v", "default_plan": "free", "plans": {}}`, "negative running things": strings.Replace(base, `"running_things": 3,`, `"running_things": -3,`, 1), "pack adds negative amount": strings.Replace(base, `{"email_per_day": 1000}`, `{"email_per_day": -1000}`, 1), } for name, body := range cases { if body == base { t.Fatalf("%s: replacement did not apply", name) } if _, err := ParseCatalog([]byte(body)); err == nil { t.Errorf("%s: ParseCatalog accepted it", name) } } } func TestCatalogIDChangesWithContent(t *testing.T) { base := exampleBytes(t) a, err := ParseCatalog([]byte(base)) if err != nil { t.Fatal(err) } again, err := ParseCatalog([]byte(base)) if err != nil { t.Fatal(err) } if a.ID() != again.ID() { t.Fatalf("same bytes, different IDs: %s vs %s", a.ID(), again.ID()) } if !strings.HasPrefix(a.ID(), "2026-10-20-v1@") || len(a.ID()) != len("2026-10-20-v1@")+12 { t.Fatalf("ID shape: %q", a.ID()) } changed, err := ParseCatalog([]byte(strings.Replace(base, `"running_things": 3,`, `"running_things": 4,`, 1))) if err != nil { t.Fatal(err) } if changed.ID() == a.ID() { t.Fatal("content changed without a version bump but the ID did not move") } reformatted := strings.Join(strings.Fields(base), " ") reformatted = strings.Replace(reformatted, `"free_accounts_per_person": 1, "plans"`, `"plans"`, 1) reformatted = strings.Replace(reformatted, `"version": "2026-10-20-v1",`, `"free_accounts_per_person": 1, "version": "2026-10-20-v1",`, 1) r, err := ParseCatalog([]byte(reformatted)) if err != nil { t.Fatal(err) } if r.ID() != a.ID() { t.Fatalf("whitespace and key order changed the ID: %s vs %s", r.ID(), a.ID()) } } func TestEffectivePlanOnly(t *testing.T) { c := exampleCatalog(t) got, err := c.Effective("free", nil, nil) if err != nil { t.Fatal(err) } if got != c.Plans["free"].Limits { t.Fatalf("got %+v, want %+v", got, c.Plans["free"].Limits) } } func TestEffectiveAddsPacks(t *testing.T) { c := exampleCatalog(t) got, err := c.Effective("personal", map[string]int{"apps": 2, "email": 1}, nil) if err != nil { t.Fatal(err) } if got.RunningThings != 35 || got.EmailPerDay != 2000 { t.Fatalf("running_things %d (want 35), email_per_day %d (want 2000)", got.RunningThings, got.EmailPerDay) } } func TestEffectiveClampsToCeilings(t *testing.T) { c := exampleCatalog(t) got, err := c.Effective("personal", map[string]int{"apps": 20, "email": 20}, nil) if err != nil { t.Fatal(err) } if got.RunningThings != 125 { t.Fatalf("running_things %d, want 125", got.RunningThings) } if got.EmailPerDay != 10000 { t.Fatalf("email_per_day %d, want the 10000 ceiling", got.EmailPerDay) } c.Ceilings.RunningThings = 100 got, err = c.Effective("personal", map[string]int{"apps": 20}, nil) if err != nil { t.Fatal(err) } if got.RunningThings != 100 { t.Fatalf("running_things %d, want the 100 ceiling", got.RunningThings) } } func TestEffectiveRefuses(t *testing.T) { c := exampleCatalog(t) cases := []struct { name string plan string packs map[string]int want error }{ {"unknown plan", "gold", nil, ErrUnknownPlan}, {"unknown pack", "personal", map[string]int{"seats": 1}, ErrUnknownPack}, {"pack not offered", "free", map[string]int{"apps": 1}, ErrPackNotOffered}, {"above max", "personal", map[string]int{"apps": 21}, ErrPackQuantity}, {"negative", "personal", map[string]int{"apps": -1}, ErrPackQuantity}, {"zero", "personal", map[string]int{"apps": 0}, ErrPackQuantity}, } for _, tc := range cases { if _, err := c.Effective(tc.plan, tc.packs, nil); !errors.Is(err, tc.want) { t.Errorf("%s: err %v, want %v", tc.name, err, tc.want) } } } func TestEffectiveSponsoredTakesTheMoreGenerous(t *testing.T) { c := exampleCatalog(t) got, err := c.Effective("personal", nil, &Limits{RunningThings: 300, EmailPerDay: 20}) if err != nil { t.Fatal(err) } if got.RunningThings != 300 { t.Fatalf("running_things %d, want 300 (sponsored is not clamped)", got.RunningThings) } if got.EmailPerDay != 1000 { t.Fatalf("email_per_day %d, want the plan's 1000", got.EmailPerDay) } got, err = c.Effective("free", nil, &Limits{CustomDomains: true, MemoryTier: "default"}) if err != nil { t.Fatal(err) } if !got.CustomDomains || got.MemoryTier != "default" { t.Fatalf("custom_domains %v, memory_tier %q", got.CustomDomains, got.MemoryTier) } } func TestEffectiveSponsoredZeroIsUnset(t *testing.T) { c := exampleCatalog(t) got, err := c.Effective("free", nil, &Limits{RunningThings: 40}) if err != nil { t.Fatal(err) } if got.MinScheduleSeconds != 900 || got.RunningThings != 40 { t.Fatalf("min_schedule_seconds %d (want 900), running_things %d (want 40)", got.MinScheduleSeconds, got.RunningThings) } got, err = c.Effective("free", nil, &Limits{MinScheduleSeconds: 60}) if err != nil { t.Fatal(err) } if got.MinScheduleSeconds != 60 { t.Fatalf("min_schedule_seconds %d, want 60", got.MinScheduleSeconds) } } // Astra plans-1 finding 5: an accepted catalog with a huge pack increment // must never wrap to a negative limit that the ceiling cannot correct. func TestEffectiveRefusesOverflow(t *testing.T) { c := exampleCatalog(t) pk := c.Packs["storage"] pk.Adds.StorageBytes = 1 << 62 c.Packs["storage"] = pk c.Ceilings.StorageBytes = 100 got, err := c.Effective("personal", map[string]int{"storage": 2}, nil) if !errors.Is(err, ErrPackQuantity) { t.Fatalf("err %v (limits %+v), want ErrPackQuantity", err, got) } }