Files
pulse/pkg/licensing/features_test.go
T
rcourtman 6be2af1c19 Let providers evaluate MSP without asking permission first
Two mandatory round-trips stood between an interested MSP and their first
screen, and neither was technical.

setup.sh required four image digests shipped as literal <pin>
placeholders, so the only way to get them was to ask. All four images are
publicly readable, so there was never anything to hand out. setup.sh now
resolves each blank pin to an immutable digest from its published tag via
buildx imagetools and writes it back to .env; hand-set values are left
alone.

setup.sh then died outright without a licence file, so nobody could start
the stack, create a workspace, or see the portal until a human minted a
licence for them. The control plane already ran unlicensed via
ProviderMSPPlanSourceEnvFallback; only the installer refused. A licence
path that is set but missing is still a hard failure, since that is a
misconfiguration rather than a choice.

Unlicensed now means evaluation rather than the cheapest paid tier. The
env fallback defaulted to msp_starter, handing every unlicensed
deployment the full 5-client Starter allowance and leaving no boundary
between evaluating and buying. Adds msp_eval at 2 workspaces: same
capabilities, smaller cap, not purchasable, not on the public ladder.

An isolation guarantee is the one claim a provider cannot evaluate from a
screenshot, and both MSP leads this year went quiet at exactly this step.

Contracts: cloud-paid records the unlicensed plan rule and the
strictly-below-paid invariant; deployment-installability records
credential-free, correspondence-free installability.

Verification: TestMSPEvalCapStaysBelowCheapestPaidTier,
TestCanonicalizePlanVersion_MSPEval,
TestProviderMSPSetupScriptSupportsUnlicensedEvaluation. The last was
negative-tested by reintroducing a <pin> placeholder and confirming it
fails. ensure_image_pins exercised against the live registries.
licensing, cloudcp, control-plane and installtests all green.
2026-08-03 16:31:24 +01:00

908 lines
28 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, FeatureAdvancedSSO, 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 TestPatrolGovernedFixesStayProFeature(t *testing.T) {
if !TierHasFeature(TierFree, FeatureAIPatrol) {
t.Fatal("community tier must include watch-only Patrol with BYOK")
}
if TierHasFeature(TierFree, FeatureAIAutoFix) {
t.Fatal("community tier must not include governed Patrol fixes")
}
if TierHasFeature(TierRelay, FeatureAIAutoFix) {
t.Fatal("relay tier must not include governed Patrol fixes")
}
if !TierHasFeature(TierPro, FeatureAIAutoFix) {
t.Fatal("pro tier must include governed Patrol fixes")
}
}
func TestWhiteLabelBrandingRemainsExplicitEnterpriseEntitlement(t *testing.T) {
if TierHasFeature(TierFree, FeatureWhiteLabel) {
t.Fatal("community tier must not include white_label report branding")
}
if TierHasFeature(TierMSP, FeatureWhiteLabel) {
t.Fatal("MSP tier defaults must not imply white_label unless the license grants it")
}
if !TierHasFeature(TierEnterprise, FeatureWhiteLabel) {
t.Fatal("enterprise tier must keep white_label report branding entitlement")
}
got := DeriveCapabilitiesFromTier(TierMSP, []string{FeatureWhiteLabel})
found := false
for _, feature := range got {
if feature == FeatureWhiteLabel {
found = true
break
}
}
if !found {
t.Fatal("explicit white_label grants must be merged into MSP capabilities")
}
}
func TestSelfHostedPaidFeatureClaimMatrix(t *testing.T) {
tests := []struct {
name string
tier Tier
wantHistoryDays int
wantIncludedCapabilities []string
wantExcludedCapabilities []string
}{
{
name: "community keeps core monitoring free and does not claim paid extras",
tier: TierFree,
wantHistoryDays: 7,
wantIncludedCapabilities: []string{
FeatureUpdateAlerts,
FeatureSSO,
FeatureAdvancedSSO,
FeatureAIPatrol,
},
wantExcludedCapabilities: []string{
FeatureRelay,
FeatureMobileApp,
FeaturePushNotifications,
FeatureLongTermMetrics,
FeatureAIAlerts,
FeatureAIAutoFix,
FeatureRBAC,
FeatureAdvancedReporting,
},
},
{
name: "relay sells remote access mobile handoff push and fourteen day history",
tier: TierRelay,
wantHistoryDays: 14,
wantIncludedCapabilities: []string{
FeatureUpdateAlerts,
FeatureSSO,
FeatureAdvancedSSO,
FeatureAIPatrol,
FeatureRelay,
FeatureMobileApp,
FeaturePushNotifications,
FeatureLongTermMetrics,
},
wantExcludedCapabilities: []string{
FeatureAIAlerts,
FeatureAIAutoFix,
FeatureRBAC,
FeatureAdvancedReporting,
FeatureAgentProfiles,
},
},
{
name: "pro sells operator extras and preserves relay capabilities",
tier: TierPro,
wantHistoryDays: 90,
wantIncludedCapabilities: []string{
FeatureRelay,
FeatureMobileApp,
FeaturePushNotifications,
FeatureLongTermMetrics,
FeatureAIAlerts,
FeatureAIAutoFix,
FeatureRBAC,
FeatureAuditLogging,
FeatureAdvancedReporting,
FeatureAgentProfiles,
},
wantExcludedCapabilities: []string{
FeatureMultiTenant,
FeatureUnlimited,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
gotHistoryDays, ok := TierHistoryDays[tt.tier]
if !ok {
t.Fatalf("TierHistoryDays missing entry for tier %q", tt.tier)
}
if gotHistoryDays != tt.wantHistoryDays {
t.Fatalf(
"TierHistoryDays[%q] = %d, want %d",
tt.tier,
gotHistoryDays,
tt.wantHistoryDays,
)
}
capabilities := make(map[string]struct{})
for _, capability := range TierFeatures[tt.tier] {
capabilities[capability] = struct{}{}
}
for _, capability := range tt.wantIncludedCapabilities {
if _, ok := capabilities[capability]; !ok {
t.Fatalf("tier %q missing paid-feature claim capability %q", tt.tier, capability)
}
}
for _, capability := range tt.wantExcludedCapabilities {
if _, ok := capabilities[capability]; ok {
t.Fatalf("tier %q unexpectedly includes capability %q", tt.tier, capability)
}
}
})
}
}
func TestDeriveEntitlements(t *testing.T) {
caps, limits := DeriveEntitlements(TierPro, []string{"custom"}, 50, 100)
if len(caps) == 0 {
t.Error("DeriveEntitlements() returned no capabilities")
}
if _, ok := limits["max_monitored_systems"]; ok {
t.Errorf("DeriveEntitlements() exposed retired max_monitored_systems limit: %v", limits)
}
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, FeatureAdvancedSSO, 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, "Patrol Investigates Issues and Explains the Root Cause"},
{FeatureAIAutoFix, "Patrol Applies Safe Fixes and Verifies the Result"},
{FeatureKubernetesAI, "Kubernetes AI Analysis (Compatibility)"},
{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, "Hosted Capacity Policy"},
{FeatureAgentProfiles, "Centralized Agent Profiles"},
{FeatureAuditLogging, "Audit Logging"},
{FeatureSSO, "Core SSO (OIDC/SAML)"},
{FeatureAdvancedSSO, "Multi-Provider SSO"},
{FeatureRelay, "Pulse Relay (Remote Access)"},
{FeatureMobileApp, "Pulse Mobile Pairing"},
{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 TestIsCompatibilityOnlyFeature(t *testing.T) {
if !IsCompatibilityOnlyFeature(FeatureKubernetesAI) {
t.Fatalf("expected %q to remain compatibility-only", FeatureKubernetesAI)
}
if IsCompatibilityOnlyFeature(FeatureAIAlerts) {
t.Fatalf("did not expect %q to be compatibility-only", FeatureAIAlerts)
}
}
func TestSelfHostedFeatureMetadataKeepsCanonicalPlanLabelsAndVisibility(t *testing.T) {
updateAlerts, ok := GetFeatureMetadata(FeatureUpdateAlerts)
if !ok {
t.Fatalf("expected metadata for %q", FeatureUpdateAlerts)
}
if updateAlerts.DisplayName != "Update Alerts (Container/Package Updates)" {
t.Fatalf("DisplayName = %q, want detailed runtime label", updateAlerts.DisplayName)
}
if updateAlerts.ComparisonName != "Update Alerts" {
t.Fatalf("ComparisonName = %q, want customer-facing label", updateAlerts.ComparisonName)
}
relay, ok := GetFeatureMetadata(FeatureRelay)
if !ok {
t.Fatalf("expected metadata for %q", FeatureRelay)
}
if relay.ComparisonName != "Pulse Relay (Remote Access)" {
t.Fatalf("ComparisonName = %q, want canonical Relay marketing label", relay.ComparisonName)
}
if GetSelfHostedFeatureRole(FeatureRelay, TierRelay) != SelfHostedFeatureRolePrimaryPillar {
t.Fatalf("expected %q to stay a Relay primary pillar", FeatureRelay)
}
multiTenant, ok := GetFeatureMetadata(FeatureMultiTenant)
if !ok {
t.Fatalf("expected metadata for %q", FeatureMultiTenant)
}
if multiTenant.DisplayableInPlanUI {
t.Fatalf("did not expect MSP-only %q to be displayable in self-hosted plan UI", FeatureMultiTenant)
}
if GetSelfHostedFeatureRole(FeatureMultiTenant, TierPro) != SelfHostedFeatureRoleHidden {
t.Fatalf("expected %q to stay hidden from the self-hosted Pro plan role", FeatureMultiTenant)
}
kubernetes, ok := GetFeatureMetadata(FeatureKubernetesAI)
if !ok {
t.Fatalf("expected metadata for %q", FeatureKubernetesAI)
}
if kubernetes.DisplayableInPlanUI {
t.Fatalf("did not expect compatibility-only %q to be displayable in plan UI", FeatureKubernetesAI)
}
}
func TestSelfHostedFeatureMetadataMatchesRuntimeTierFeatures(t *testing.T) {
for _, tier := range []Tier{TierFree, TierRelay, TierPro, TierProPlus, TierProAnnual, TierLifetime} {
tier := tier
t.Run(string(tier), func(t *testing.T) {
got := sortedFeatureSet(TierFeatures[tier])
want := selfHostedMetadataFeatureKeysForTier(tier)
if strings.Join(got, "\x00") != strings.Join(want, "\x00") {
t.Fatalf("TierFeatures[%q]=%v, want self-hosted metadata features %v", tier, got, want)
}
})
}
}
func selfHostedMetadataFeatureKeysForTier(tier Tier) []string {
out := make([]string, 0)
for _, entry := range AllFeatureMetadata() {
if GetSelfHostedFeatureRole(entry.Key, tier) == SelfHostedFeatureRoleHidden {
continue
}
out = append(out, entry.Key)
}
return sortedFeatureSet(out)
}
func sortedFeatureSet(features []string) []string {
seen := make(map[string]struct{}, len(features))
for _, feature := range features {
feature = strings.TrimSpace(feature)
if feature == "" {
continue
}
seen[feature] = struct{}{}
}
out := make([]string, 0, len(seen))
for feature := range seen {
out = append(out, feature)
}
sort.Strings(out)
return out
}
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)
}
}
// Pro+ is a legacy continuity tier, not a bigger self-hosted monitoring tier.
proPlusSet := asSet(TierFeatures[TierProPlus])
if len(proPlusSet) != len(proSet) {
t.Fatalf("Pro+ feature count = %d, want Pro feature count %d", len(proPlusSet), len(proSet))
}
for f := range proSet {
if !proPlusSet[f] {
t.Errorf("Pro+ tier missing Pro 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)
}
}
}
// TestAllTiersHaveHistoryDays ensures every tier in TierFeatures also has a
// metrics-retention entry. Core monitoring volume limits are retired.
func TestAllTiersHaveHistoryDays(t *testing.T) {
for tier := range TierFeatures {
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", 5},
{"msp_hosted_v1", 5},
{"msp_growth", 15},
{"msp_scale", 40},
}
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 hosted workspace plans
// still carry a workspace limit entry after monitored-system caps were retired.
func TestCloudWorkspacePlansHaveWorkspaceLimits(t *testing.T) {
for plan := range CloudPlanWorkspaceLimits {
if !strings.HasPrefix(plan, "cloud_") && !strings.HasPrefix(plan, "msp_") {
t.Errorf("CloudPlanWorkspaceLimits includes non-hosted plan %q", 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)
}
})
}
}
func TestIsGrandfatheredRecurringV5PlanVersion(t *testing.T) {
tests := []struct {
plan string
want bool
}{
{plan: "v5_pro_monthly_grandfathered", want: true},
{plan: "v5_pro_annual_grandfathered", want: true},
{plan: "price_1ShIsdBrHBocJIGH71yQusLG", want: false},
{plan: "cloud_starter", want: false},
{plan: "", want: false},
}
for _, tt := range tests {
t.Run(tt.plan, func(t *testing.T) {
if got := IsGrandfatheredRecurringV5PlanVersion(tt.plan); got != tt.want {
t.Fatalf("IsGrandfatheredRecurringV5PlanVersion(%q) = %v, want %v", tt.plan, got, tt.want)
}
})
}
}
func TestIsSelfHostedCommunityPlanVersion(t *testing.T) {
tests := []struct {
plan string
want bool
}{
{plan: "community", want: true},
{plan: "Community", want: true},
{plan: "free", want: true},
{plan: "relay", want: false},
{plan: "pro", want: false},
{plan: "pro_plus", want: false},
{plan: "pro_annual", want: false},
{plan: "lifetime", want: false},
{plan: "v5_lifetime_grandfathered", want: false},
{plan: "cloud_starter", want: false},
{plan: "trial", want: false},
{plan: "pro-v2", want: false},
{plan: "", want: false},
}
for _, tt := range tests {
t.Run(tt.plan, func(t *testing.T) {
if got := IsSelfHostedCommunityPlanVersion(tt.plan); got != tt.want {
t.Fatalf("IsSelfHostedCommunityPlanVersion(%q) = %v, want %v", tt.plan, got, tt.want)
}
})
}
}
func TestIsSelfHostedCoreMonitoringUncappedPlanVersion(t *testing.T) {
tests := []struct {
plan string
want bool
}{
{plan: "community", want: true},
{plan: "free", want: true},
{plan: "relay", want: true},
{plan: "pro", want: true},
{plan: "pro_plus", want: true},
{plan: "pro_annual", want: true},
{plan: "lifetime", want: true},
{plan: "v5_lifetime_grandfathered", want: true},
{plan: "v5_pro_monthly_grandfathered", want: true},
{plan: "v5_pro_annual_grandfathered", want: true},
{plan: "cloud_starter", want: false},
{plan: "msp_starter", want: false},
{plan: "pro-v2", want: false},
{plan: "", want: false},
}
for _, tt := range tests {
t.Run(tt.plan, func(t *testing.T) {
if got := IsSelfHostedCoreMonitoringUncappedPlanVersion(tt.plan); got != tt.want {
t.Fatalf("IsSelfHostedCoreMonitoringUncappedPlanVersion(%q) = %v, want %v", tt.plan, got, tt.want)
}
})
}
}
func TestIsSelfHostedCoreMonitoringUncappedTier(t *testing.T) {
tests := []struct {
tier Tier
want bool
}{
{tier: TierFree, want: true},
{tier: TierRelay, want: true},
{tier: TierPro, want: true},
{tier: TierProPlus, want: true},
{tier: TierProAnnual, want: true},
{tier: TierLifetime, want: true},
{tier: TierCloud, want: false},
{tier: TierMSP, want: false},
{tier: TierEnterprise, want: false},
{tier: Tier(" pro "), want: true},
{tier: Tier("unknown"), want: false},
}
for _, tt := range tests {
t.Run(string(tt.tier), func(t *testing.T) {
if got := IsSelfHostedCoreMonitoringUncappedTier(tt.tier); got != tt.want {
t.Fatalf("IsSelfHostedCoreMonitoringUncappedTier(%q) = %v, want %v", tt.tier, got, tt.want)
}
})
}
}
// TestPriceIDToPlanVersion_HostedPlansHaveWorkspaceLimits ensures hosted plans
// in the price map retain workspace policy after monitored-system caps retire.
func TestPriceIDToPlanVersion_AllMapToKnownPlans(t *testing.T) {
for priceID, plan := range PriceIDToPlanVersion {
t.Run(priceID, func(t *testing.T) {
if !strings.HasPrefix(plan, "cloud_") && !strings.HasPrefix(plan, "msp_") {
return
}
if _, known := WorkspaceLimitForPlan(plan); !known {
t.Errorf("PriceIDToPlanVersion[%q] = %q, but WorkspaceLimitForPlan does not recognize it", priceID, plan)
}
})
}
}
// TestBusinessTierContractShape pins the dormant Business tier's contract:
// Pro's exact feature set (differentiation is by max_users limit, retention,
// and support, never by features), 365-day history, uncapped self-hosted core
// monitoring, recognized plan versions, and the Business display name. See
// cloud-paid Extension Point 26.
func TestBusinessTierContractShape(t *testing.T) {
proSet := TierFeatures[TierPro]
businessSet := TierFeatures[TierBusiness]
if len(proSet) != len(businessSet) {
t.Fatalf("Business features (%d) must equal Pro features (%d)", len(businessSet), len(proSet))
}
for i, feature := range proSet {
if businessSet[i] != feature {
t.Fatalf("Business feature[%d] = %q, want Pro's %q", i, businessSet[i], feature)
}
}
if got := TierHistoryDays[TierBusiness]; got != 365 {
t.Fatalf("TierHistoryDays[TierBusiness] = %d, want 365", got)
}
if !IsSelfHostedCoreMonitoringUncappedTier(TierBusiness) {
t.Fatal("TierBusiness must be self-hosted core-monitoring uncapped")
}
for _, plan := range []string{"business", "business_annual"} {
if !IsSelfHostedCoreMonitoringUncappedPlanVersion(plan) {
t.Fatalf("plan version %q must be self-hosted core-monitoring uncapped", plan)
}
}
if got := GetTierDisplayName(TierBusiness); got != "Business" {
t.Fatalf("GetTierDisplayName(TierBusiness) = %q, want Business", got)
}
}
// The unlicensed evaluation cap is a commercial boundary, not a default that
// happens to be small. If it ever reaches the cheapest paid tier there is
// nothing left to sell, which is exactly the state this replaced: the
// unlicensed control plane used to run on the full 5-client Starter allowance.
func TestMSPEvalCapStaysBelowCheapestPaidTier(t *testing.T) {
evalLimit, known := WorkspaceLimitForPlan(PlanVersionMSPEval)
if !known {
t.Fatalf("WorkspaceLimitForPlan(%q) unknown; an unrecognized plan fails closed to %d and breaks provider preflight", PlanVersionMSPEval, UnknownPlanDefaultWorkspaceLimit)
}
if evalLimit != 2 {
t.Fatalf("eval workspace limit = %d, want 2", evalLimit)
}
for _, paid := range []string{"msp_starter", "msp_growth", "msp_scale"} {
paidLimit, paidKnown := WorkspaceLimitForPlan(paid)
if !paidKnown {
t.Fatalf("paid plan %q has no workspace limit", paid)
}
if evalLimit >= paidLimit {
t.Fatalf("eval limit %d is not below paid plan %q limit %d", evalLimit, paid, paidLimit)
}
}
}