mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
590 lines
17 KiB
Go
590 lines
17 KiB
Go
package licensing
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDeriveCapabilitiesFromTier(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tier Tier
|
|
explicitFeatures []string
|
|
wantLen int
|
|
wantContains []string
|
|
wantNotContains []string
|
|
}{
|
|
{
|
|
name: "free_tier_includes_base_features",
|
|
tier: TierFree,
|
|
explicitFeatures: nil,
|
|
wantLen: len(freeFeatures),
|
|
wantContains: []string{FeatureUpdateAlerts, FeatureSSO, FeatureAIPatrol},
|
|
wantNotContains: []string{FeatureRelay, FeatureAIAutoFix, FeatureLongTermMetrics},
|
|
},
|
|
{
|
|
name: "relay_tier_includes_remote_access",
|
|
tier: TierRelay,
|
|
explicitFeatures: nil,
|
|
wantLen: len(relayFeatures),
|
|
wantContains: []string{FeatureRelay, FeatureMobileApp, FeaturePushNotifications, FeatureLongTermMetrics},
|
|
wantNotContains: []string{FeatureAIAutoFix, FeatureAIAlerts, FeatureRBAC},
|
|
},
|
|
{
|
|
name: "pro_tier_includes_all_ai_and_compliance",
|
|
tier: TierPro,
|
|
explicitFeatures: nil,
|
|
wantLen: len(proFeatures),
|
|
wantContains: []string{FeatureAIAutoFix, FeatureAIAlerts, FeatureRBAC, FeatureAuditLogging, FeatureRelay},
|
|
wantNotContains: []string{FeatureUnlimited, FeatureMultiTenant},
|
|
},
|
|
{
|
|
name: "pro_plus_same_features_as_pro",
|
|
tier: TierProPlus,
|
|
explicitFeatures: nil,
|
|
wantLen: len(proFeatures),
|
|
wantContains: []string{FeatureAIAutoFix, FeatureRelay, FeatureRBAC},
|
|
},
|
|
{
|
|
name: "pro_annual_legacy_same_as_pro",
|
|
tier: TierProAnnual,
|
|
explicitFeatures: nil,
|
|
wantLen: len(proFeatures),
|
|
wantContains: []string{FeatureAIAutoFix, FeatureRelay},
|
|
},
|
|
{
|
|
name: "lifetime_legacy_same_as_pro",
|
|
tier: TierLifetime,
|
|
explicitFeatures: nil,
|
|
wantLen: len(proFeatures),
|
|
wantContains: []string{FeatureAIAutoFix, FeatureRelay},
|
|
},
|
|
{
|
|
name: "cloud_includes_pro_features",
|
|
tier: TierCloud,
|
|
explicitFeatures: nil,
|
|
wantLen: len(proFeatures),
|
|
wantContains: []string{FeatureAIAutoFix, FeatureRelay, FeatureLongTermMetrics},
|
|
},
|
|
{
|
|
name: "msp_tier_includes_unlimited_and_multi_tenant",
|
|
tier: TierMSP,
|
|
explicitFeatures: nil,
|
|
wantLen: len(mspFeatures),
|
|
wantContains: []string{FeatureUnlimited, FeatureMultiTenant},
|
|
},
|
|
{
|
|
name: "enterprise_tier_includes_white_label",
|
|
tier: TierEnterprise,
|
|
explicitFeatures: nil,
|
|
wantLen: len(enterpriseFeatures),
|
|
wantContains: []string{FeatureMultiTenant, FeatureWhiteLabel, FeatureMultiUser},
|
|
},
|
|
{
|
|
name: "explicit_features_merged",
|
|
tier: TierFree,
|
|
explicitFeatures: []string{"custom_feature"},
|
|
wantContains: []string{"custom_feature", FeatureUpdateAlerts},
|
|
},
|
|
{
|
|
name: "unknown_tier_returns_empty",
|
|
tier: Tier("unknown"),
|
|
explicitFeatures: nil,
|
|
wantLen: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := DeriveCapabilitiesFromTier(tt.tier, tt.explicitFeatures)
|
|
if tt.wantLen > 0 && len(got) != tt.wantLen {
|
|
t.Errorf("DeriveCapabilitiesFromTier() returned %d capabilities, want %d", len(got), tt.wantLen)
|
|
}
|
|
gotSet := make(map[string]bool)
|
|
for _, f := range got {
|
|
gotSet[f] = true
|
|
}
|
|
for _, f := range tt.wantContains {
|
|
if !gotSet[f] {
|
|
t.Errorf("DeriveCapabilitiesFromTier() missing expected feature %q", f)
|
|
}
|
|
}
|
|
for _, f := range tt.wantNotContains {
|
|
if gotSet[f] {
|
|
t.Errorf("DeriveCapabilitiesFromTier() should NOT include feature %q for tier %q", f, tt.tier)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeriveCapabilitiesFromTier_Sorted(t *testing.T) {
|
|
got := DeriveCapabilitiesFromTier(TierPro, nil)
|
|
if !sort.StringsAreSorted(got) {
|
|
t.Errorf("DeriveCapabilitiesFromTier() should return sorted capabilities")
|
|
}
|
|
}
|
|
|
|
func TestDeriveEntitlements(t *testing.T) {
|
|
caps, limits := DeriveEntitlements(TierPro, []string{"custom"}, 50, 100)
|
|
|
|
if len(caps) == 0 {
|
|
t.Error("DeriveEntitlements() returned no capabilities")
|
|
}
|
|
|
|
if limits["max_monitored_systems"] != 50 {
|
|
t.Errorf("max_monitored_systems limit = %d, want 50", limits["max_monitored_systems"])
|
|
}
|
|
if limits["max_guests"] != 100 {
|
|
t.Errorf("max_guests limit = %d, want 100", limits["max_guests"])
|
|
}
|
|
}
|
|
|
|
func TestDeriveEntitlements_ZeroLimitsNotIncluded(t *testing.T) {
|
|
_, limits := DeriveEntitlements(TierPro, nil, 0, 0)
|
|
|
|
if _, ok := limits["max_monitored_systems"]; ok {
|
|
t.Error("max_monitored_systems should not be in limits when 0")
|
|
}
|
|
if _, ok := limits["max_guests"]; ok {
|
|
t.Error("max_guests should not be in limits when 0")
|
|
}
|
|
}
|
|
|
|
func TestTierHasFeature(t *testing.T) {
|
|
tests := []struct {
|
|
tier Tier
|
|
feature string
|
|
want bool
|
|
}{
|
|
{TierPro, FeatureAIAutoFix, true},
|
|
{TierPro, FeatureRBAC, true},
|
|
{TierPro, FeatureRelay, true},
|
|
{TierFree, FeatureAIAutoFix, false},
|
|
{TierFree, FeatureRelay, false},
|
|
{TierFree, FeatureUpdateAlerts, true},
|
|
{TierFree, FeatureAIPatrol, true},
|
|
{TierRelay, FeatureRelay, true},
|
|
{TierRelay, FeatureMobileApp, true},
|
|
{TierRelay, FeaturePushNotifications, true},
|
|
{TierRelay, FeatureLongTermMetrics, true},
|
|
{TierRelay, FeatureAIAutoFix, false},
|
|
{TierRelay, FeatureRBAC, false},
|
|
{TierProPlus, FeatureAIAutoFix, true},
|
|
{TierProPlus, FeatureRelay, true},
|
|
{TierEnterprise, FeatureMultiTenant, true},
|
|
{Tier("unknown"), FeatureAIAutoFix, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(string(tt.tier)+"_"+tt.feature, func(t *testing.T) {
|
|
got := TierHasFeature(tt.tier, tt.feature)
|
|
if got != tt.want {
|
|
t.Errorf("TierHasFeature(%q, %q) = %v, want %v", tt.tier, tt.feature, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetTierDisplayName(t *testing.T) {
|
|
tests := []struct {
|
|
tier Tier
|
|
want string
|
|
}{
|
|
{TierFree, "Community"},
|
|
{TierRelay, "Relay"},
|
|
{TierPro, "Pro"},
|
|
{TierProPlus, "Pro+"},
|
|
{TierProAnnual, "Pro (Annual)"},
|
|
{TierLifetime, "Pro (Lifetime)"},
|
|
{TierCloud, "Cloud"},
|
|
{TierMSP, "MSP"},
|
|
{TierEnterprise, "Enterprise"},
|
|
{Tier("unknown"), "Unknown"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(string(tt.tier), func(t *testing.T) {
|
|
got := GetTierDisplayName(tt.tier)
|
|
if got != tt.want {
|
|
t.Errorf("GetTierDisplayName(%q) = %q, want %q", tt.tier, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetFeatureDisplayName(t *testing.T) {
|
|
tests := []struct {
|
|
feature string
|
|
want string
|
|
}{
|
|
{FeatureAIPatrol, "Pulse Patrol (Background Health Checks)"},
|
|
{FeatureAIAlerts, "Alert Analysis"},
|
|
{FeatureAIAutoFix, "Pulse Patrol Auto-Fix"},
|
|
{FeatureKubernetesAI, "Kubernetes Analysis"},
|
|
{FeatureUpdateAlerts, "Update Alerts (Container/Package Updates)"},
|
|
{FeatureRBAC, "Role-Based Access Control (RBAC)"},
|
|
{FeatureMultiUser, "Multi-User Mode"},
|
|
{FeatureWhiteLabel, "White-Label Branding"},
|
|
{FeatureMultiTenant, "Multi-Tenant Mode"},
|
|
{FeatureUnlimited, "Unlimited Instances"},
|
|
{FeatureAgentProfiles, "Centralized Agent Profiles"},
|
|
{FeatureAuditLogging, "Audit Logging"},
|
|
{FeatureSSO, "Basic SSO (OIDC)"},
|
|
{FeatureAdvancedSSO, "Advanced SSO (SAML/Multi-Provider)"},
|
|
{FeatureRelay, "Pulse Relay (Remote Access)"},
|
|
{FeatureMobileApp, "Mobile App Access"},
|
|
{FeaturePushNotifications, "Push Notifications"},
|
|
{FeatureAdvancedReporting, "PDF/CSV Reporting"},
|
|
{FeatureLongTermMetrics, "Extended Metric History"},
|
|
{"unknown_feature", "unknown_feature"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.feature, func(t *testing.T) {
|
|
got := GetFeatureDisplayName(tt.feature)
|
|
if got != tt.want {
|
|
t.Errorf("GetFeatureDisplayName(%q) = %q, want %q", tt.feature, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTierMonitoredSystemLimits(t *testing.T) {
|
|
tests := []struct {
|
|
tier Tier
|
|
want int
|
|
}{
|
|
{TierFree, 5},
|
|
{TierRelay, 8},
|
|
{TierPro, 15},
|
|
{TierProPlus, 50},
|
|
{TierProAnnual, 15},
|
|
{TierLifetime, 15},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(string(tt.tier), func(t *testing.T) {
|
|
got, ok := TierMonitoredSystemLimits[tt.tier]
|
|
if !ok {
|
|
t.Fatalf("TierMonitoredSystemLimits missing entry for tier %q", tt.tier)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("TierMonitoredSystemLimits[%q] = %d, want %d", tt.tier, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTierHistoryDays(t *testing.T) {
|
|
tests := []struct {
|
|
tier Tier
|
|
want int
|
|
}{
|
|
{TierFree, 7},
|
|
{TierRelay, 14},
|
|
{TierPro, 90},
|
|
{TierProPlus, 90},
|
|
{TierCloud, 90},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(string(tt.tier), func(t *testing.T) {
|
|
got, ok := TierHistoryDays[tt.tier]
|
|
if !ok {
|
|
t.Fatalf("TierHistoryDays missing entry for tier %q", tt.tier)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("TierHistoryDays[%q] = %d, want %d", tt.tier, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTierFeatureInheritance verifies that higher tiers include all features from lower tiers.
|
|
func TestTierFeatureInheritance(t *testing.T) {
|
|
asSet := func(features []string) map[string]bool {
|
|
s := make(map[string]bool, len(features))
|
|
for _, f := range features {
|
|
s[f] = true
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Relay must include all Free features.
|
|
freeSet := asSet(TierFeatures[TierFree])
|
|
relaySet := asSet(TierFeatures[TierRelay])
|
|
for f := range freeSet {
|
|
if !relaySet[f] {
|
|
t.Errorf("Relay tier missing Free feature %q", f)
|
|
}
|
|
}
|
|
|
|
// Pro must include all Relay features.
|
|
proSet := asSet(TierFeatures[TierPro])
|
|
for f := range relaySet {
|
|
if !proSet[f] {
|
|
t.Errorf("Pro tier missing Relay feature %q", f)
|
|
}
|
|
}
|
|
|
|
// MSP must include all Pro features.
|
|
mspSet := asSet(TierFeatures[TierMSP])
|
|
for f := range proSet {
|
|
if !mspSet[f] {
|
|
t.Errorf("MSP tier missing Pro feature %q", f)
|
|
}
|
|
}
|
|
|
|
// Enterprise must include all MSP features.
|
|
entSet := asSet(TierFeatures[TierEnterprise])
|
|
for f := range mspSet {
|
|
if !entSet[f] {
|
|
t.Errorf("Enterprise tier missing MSP feature %q", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPublicTierDefaultsNeverIncludeInternalCapabilities(t *testing.T) {
|
|
for tier, features := range TierFeatures {
|
|
for _, feature := range features {
|
|
if !CapabilityVisibleInPublicPayload(feature) {
|
|
t.Fatalf("TierFeatures[%q] leaked internal capability %q", tier, feature)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFilterPublicCapabilitiesStripsInternalOnlyFeatures(t *testing.T) {
|
|
got := FilterPublicCapabilities([]string{
|
|
FeatureRelay,
|
|
FeatureDemoFixtures,
|
|
FeatureAIPatrol,
|
|
})
|
|
want := []string{FeatureRelay, FeatureAIPatrol}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("FilterPublicCapabilities() len=%d, want %d (%v)", len(got), len(want), got)
|
|
}
|
|
for i, feature := range want {
|
|
if got[i] != feature {
|
|
t.Fatalf("FilterPublicCapabilities()[%d]=%q, want %q", i, got[i], feature)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLimitsForCloudPlan_KnownPlans(t *testing.T) {
|
|
tests := []struct {
|
|
plan string
|
|
wantLimit int64
|
|
}{
|
|
{"cloud_starter", 10},
|
|
{"cloud_power", 30},
|
|
{"cloud_max", 75},
|
|
{"cloud_founding", 10},
|
|
{"msp_starter", 50},
|
|
{"msp_hosted_v1", 50},
|
|
{"msp_growth", 150},
|
|
{"msp_scale", 400},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.plan, func(t *testing.T) {
|
|
limits, known := LimitsForCloudPlan(tt.plan)
|
|
if !known {
|
|
t.Errorf("LimitsForCloudPlan(%q): known = false, want true", tt.plan)
|
|
}
|
|
got, ok := limits["max_monitored_systems"]
|
|
if !ok {
|
|
t.Fatalf("LimitsForCloudPlan(%q): missing max_monitored_systems key", tt.plan)
|
|
}
|
|
if got != tt.wantLimit {
|
|
t.Errorf("LimitsForCloudPlan(%q): max_monitored_systems = %d, want %d", tt.plan, got, tt.wantLimit)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLimitsForCloudPlan_UnknownPlanFailsClosed(t *testing.T) {
|
|
unknownPlans := []string{
|
|
"stripe",
|
|
"stripe_price:price_123",
|
|
"",
|
|
"unknown_plan",
|
|
"cloud_unknown",
|
|
}
|
|
|
|
for _, plan := range unknownPlans {
|
|
t.Run(plan, func(t *testing.T) {
|
|
limits, known := LimitsForCloudPlan(plan)
|
|
if known {
|
|
t.Errorf("LimitsForCloudPlan(%q): known = true, want false", plan)
|
|
}
|
|
got, ok := limits["max_monitored_systems"]
|
|
if !ok {
|
|
t.Fatalf("LimitsForCloudPlan(%q): missing max_monitored_systems key (fail-open!)", plan)
|
|
}
|
|
if got != int64(UnknownPlanDefaultMonitoredSystemLimit) {
|
|
t.Errorf("LimitsForCloudPlan(%q): max_monitored_systems = %d, want default %d", plan, got, UnknownPlanDefaultMonitoredSystemLimit)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLimitsForCloudPlan_NeverReturnsEmptyMap(t *testing.T) {
|
|
// This test ensures the fail-closed invariant: LimitsForCloudPlan must
|
|
// ALWAYS return a map with "max_monitored_systems" set, regardless of input.
|
|
inputs := []string{"cloud_starter", "stripe", "", "garbage", "msp_starter", "msp_hosted_v1"}
|
|
for _, plan := range inputs {
|
|
limits, _ := LimitsForCloudPlan(plan)
|
|
if _, ok := limits["max_monitored_systems"]; !ok {
|
|
t.Errorf("LimitsForCloudPlan(%q) returned map without max_monitored_systems — fail-open vulnerability", plan)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAllTiersHaveHostLimitsAndHistoryDays ensures every tier in TierFeatures
|
|
// also has entries in TierMonitoredSystemLimits and TierHistoryDays.
|
|
func TestAllTiersHaveHostLimitsAndHistoryDays(t *testing.T) {
|
|
for tier := range TierFeatures {
|
|
if _, ok := TierMonitoredSystemLimits[tier]; !ok {
|
|
t.Errorf("TierMonitoredSystemLimits missing entry for tier %q", tier)
|
|
}
|
|
if _, ok := TierHistoryDays[tier]; !ok {
|
|
t.Errorf("TierHistoryDays missing entry for tier %q", tier)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceLimitForPlan_KnownPlans(t *testing.T) {
|
|
tests := []struct {
|
|
plan string
|
|
wantLimit int
|
|
}{
|
|
{"cloud_starter", 1},
|
|
{"cloud_power", 1},
|
|
{"cloud_max", 1},
|
|
{"cloud_founding", 1},
|
|
{"msp_starter", 10},
|
|
{"msp_hosted_v1", 10},
|
|
{"msp_growth", 25},
|
|
{"msp_scale", 50},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.plan, func(t *testing.T) {
|
|
limit, known := WorkspaceLimitForPlan(tt.plan)
|
|
if !known {
|
|
t.Errorf("WorkspaceLimitForPlan(%q): known = false, want true", tt.plan)
|
|
}
|
|
if limit != tt.wantLimit {
|
|
t.Errorf("WorkspaceLimitForPlan(%q) = %d, want %d", tt.plan, limit, tt.wantLimit)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkspaceLimitForPlan_UnknownPlanFailsClosed(t *testing.T) {
|
|
unknownPlans := []string{"stripe", "", "unknown_plan", "cloud_unknown"}
|
|
|
|
for _, plan := range unknownPlans {
|
|
t.Run(plan, func(t *testing.T) {
|
|
limit, known := WorkspaceLimitForPlan(plan)
|
|
if known {
|
|
t.Errorf("WorkspaceLimitForPlan(%q): known = true, want false", plan)
|
|
}
|
|
if limit != UnknownPlanDefaultWorkspaceLimit {
|
|
t.Errorf("WorkspaceLimitForPlan(%q) = %d, want default %d", plan, limit, UnknownPlanDefaultWorkspaceLimit)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCloudWorkspacePlansHaveWorkspaceLimits ensures only hosted plans that
|
|
// actually support workspaces also carry a workspace limit entry.
|
|
func TestCloudWorkspacePlansHaveWorkspaceLimits(t *testing.T) {
|
|
for plan := range CloudPlanMonitoredSystemLimits {
|
|
if !strings.HasPrefix(plan, "cloud_") && !strings.HasPrefix(plan, "msp_") {
|
|
continue
|
|
}
|
|
if _, ok := CloudPlanWorkspaceLimits[plan]; !ok {
|
|
t.Errorf("CloudPlanWorkspaceLimits missing entry for plan %q (present in CloudPlanMonitoredSystemLimits)", plan)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanVersionForPriceID_KnownPrices(t *testing.T) {
|
|
tests := []struct {
|
|
priceID string
|
|
wantPlan string
|
|
}{
|
|
// Cloud Starter (monthly, annual, founding)
|
|
{"price_1T5kflBrHBocJIGHUqPv1dzV", "cloud_starter"},
|
|
{"price_1T5kfmBrHBocJIGHTS3ymKxM", "cloud_starter"},
|
|
{"price_1T5kfnBrHBocJIGHATQJr79D", "cloud_founding"},
|
|
// Cloud Power
|
|
{"price_1T5kg2BrHBocJIGHmkoF0zXY", "cloud_power"},
|
|
{"price_1T5kg3BrHBocJIGH2EtzKofV", "cloud_power"},
|
|
// Cloud Max
|
|
{"price_1T5kg4BrHBocJIGHHa8Ecqho", "cloud_max"},
|
|
{"price_1T5kg5BrHBocJIGH5AIJ4nVc", "cloud_max"},
|
|
// MSP Starter
|
|
{"price_1T5kgTBrHBocJIGHjOs15LI2", "msp_starter"},
|
|
{"price_1T5kgUBrHBocJIGHT6PiOn6x", "msp_starter"},
|
|
// MSP Growth
|
|
{"price_1T5kgVBrHBocJIGHulNsCTb1", "msp_growth"},
|
|
{"price_1T5kgWBrHBocJIGHTuaNjnJ2", "msp_growth"},
|
|
// MSP Scale
|
|
{"price_1T5kgWBrHBocJIGHo40iFeRd", "msp_scale"},
|
|
{"price_1T5kgXBrHBocJIGHWlOgTyGV", "msp_scale"},
|
|
// Grandfathered Pulse Pro recurring renewals (v5)
|
|
{"price_1ShIsdBrHBocJIGH71yQusLG", "v5_pro_monthly_grandfathered"},
|
|
{"price_1ShIsnBrHBocJIGHBKkzsZ3T", "v5_pro_annual_grandfathered"},
|
|
// Grandfathered Pulse Pro recurring renewals (legacy v1)
|
|
{"price_1SgDxvBrHBocJIGHStaGuiAX", "v5_pro_monthly_grandfathered"},
|
|
{"price_1SgDxwBrHBocJIGHTKTsIMLc", "v5_pro_annual_grandfathered"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.priceID, func(t *testing.T) {
|
|
plan, ok := PlanVersionForPriceID(tt.priceID)
|
|
if !ok {
|
|
t.Fatalf("PlanVersionForPriceID(%q): not found", tt.priceID)
|
|
}
|
|
if plan != tt.wantPlan {
|
|
t.Errorf("PlanVersionForPriceID(%q) = %q, want %q", tt.priceID, plan, tt.wantPlan)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlanVersionForPriceID_UnknownPrices(t *testing.T) {
|
|
unknowns := []string{"price_unknown", "", "not_a_price"}
|
|
for _, id := range unknowns {
|
|
t.Run(id, func(t *testing.T) {
|
|
_, ok := PlanVersionForPriceID(id)
|
|
if ok {
|
|
t.Errorf("PlanVersionForPriceID(%q): expected not found", id)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPriceIDToPlanVersion_AllMapToKnownPlans ensures every plan version in the
|
|
// price→plan map is recognized by LimitsForCloudPlan (fail-closed safety net).
|
|
func TestPriceIDToPlanVersion_AllMapToKnownPlans(t *testing.T) {
|
|
for priceID, plan := range PriceIDToPlanVersion {
|
|
t.Run(priceID, func(t *testing.T) {
|
|
_, known := LimitsForCloudPlan(plan)
|
|
if !known {
|
|
t.Errorf("PriceIDToPlanVersion[%q] = %q, but LimitsForCloudPlan does not recognize it", priceID, plan)
|
|
}
|
|
})
|
|
}
|
|
}
|