Files
pulse/pkg/licensing/entitlement_payload_test.go
T
2026-05-06 17:18:35 +01:00

539 lines
18 KiB
Go

package licensing
import (
"encoding/json"
"reflect"
"strings"
"testing"
"time"
)
func containsString(values []string, want string) bool {
for _, value := range values {
if value == want {
return true
}
}
return false
}
func hasLimit(payload EntitlementPayload, key string) bool {
for _, limit := range payload.Limits {
if limit.Key == key {
return true
}
}
return false
}
func TestBuildEntitlementPayload_ActiveLicenseOmitsRetiredMonitoredSystemLimit(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierCloud,
PlanVersion: "cloud_starter",
Features: append([]string(nil), TierFeatures[TierPro]...),
}
payload := BuildEntitlementPayload(status, "")
if payload.SubscriptionState != string(SubStateActive) {
t.Fatalf("expected subscription_state %q, got %q", SubStateActive, payload.SubscriptionState)
}
if !reflect.DeepEqual(payload.Capabilities, status.Features) {
t.Fatalf("expected capabilities to match status features")
}
if hasLimit(payload, MaxMonitoredSystemsLicenseGateKey) {
t.Fatalf("payload exposed retired monitored-system limit: %+v", payload.Limits)
}
if len(payload.UpgradeReasons) != 0 {
t.Fatalf("expected no upgrade reasons for pro tier, got %d", len(payload.UpgradeReasons))
}
}
func TestBuildEntitlementPayload_StripsInternalOnlyCapabilities(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierEnterprise,
Features: []string{
FeatureRelay,
FeatureDemoFixtures,
FeatureMultiTenant,
},
}
payload := BuildEntitlementPayload(status, "")
if reflect.DeepEqual(payload.Capabilities, status.Features) {
t.Fatalf("expected payload capabilities to filter internal-only features, got %v", payload.Capabilities)
}
if containsString(payload.Capabilities, FeatureDemoFixtures) {
t.Fatalf("payload capabilities leaked internal feature %q: %v", FeatureDemoFixtures, payload.Capabilities)
}
if !containsString(payload.Capabilities, FeatureRelay) || !containsString(payload.Capabilities, FeatureMultiTenant) {
t.Fatalf("payload capabilities lost public features: %v", payload.Capabilities)
}
}
func TestBuildEntitlementPayload_FreeTier(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierFree,
Features: append([]string(nil), TierFeatures[TierFree]...),
}
payload := BuildEntitlementPayload(status, "")
if len(payload.UpgradeReasons) == 0 {
t.Fatalf("expected upgrade reasons for free tier")
}
if payload.TrialEligible {
t.Fatal("expected retired self-hosted trial eligibility to stay false")
}
if payload.TrialEligibilityReason != "" {
t.Fatalf("TrialEligibilityReason=%q, want empty", payload.TrialEligibilityReason)
}
for _, reason := range payload.UpgradeReasons {
if reason.ActionURL == "" {
t.Fatalf("expected action_url for reason %q", reason.Key)
}
}
}
func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierCloud,
PlanVersion: "cloud_starter",
Features: append([]string(nil), TierFeatures[TierPro]...),
MaxGuests: 100,
}
payload := BuildEntitlementPayloadWithUsage(status, "", EntitlementUsageSnapshot{
MonitoredSystems: 12,
MonitoredSystemsAvailable: true,
Guests: 44,
LegacyConnections: LegacyConnectionCounts{
ProxmoxNodes: 2,
DockerHosts: 1,
KubernetesClusters: 3,
},
}, nil)
if hasLimit(payload, MaxMonitoredSystemsLicenseGateKey) {
t.Fatalf("payload exposed retired monitored-system limit: %+v", payload.Limits)
}
var guestLimit *LimitStatus
for i := range payload.Limits {
if payload.Limits[i].Key == "max_guests" {
guestLimit = &payload.Limits[i]
}
}
if guestLimit == nil {
t.Fatalf("expected max_guests limit")
}
if guestLimit.Current != 44 {
t.Fatalf("expected guest current 44, got %d", guestLimit.Current)
}
if payload.LegacyConnections.ProxmoxNodes != 2 {
t.Fatalf("expected proxmox_nodes 2, got %d", payload.LegacyConnections.ProxmoxNodes)
}
if payload.LegacyConnections.DockerHosts != 1 {
t.Fatalf("expected docker_hosts 1, got %d", payload.LegacyConnections.DockerHosts)
}
if payload.LegacyConnections.KubernetesClusters != 3 {
t.Fatalf(
"expected kubernetes_clusters 3, got %d",
payload.LegacyConnections.KubernetesClusters,
)
}
if payload.HasMigrationGap {
t.Fatal("expected has_migration_gap=false")
}
}
func TestBuildEntitlementPayload_TrialState(t *testing.T) {
expiresAt := time.Now().Add(36 * time.Hour).UTC().Format(time.RFC3339)
status := &LicenseStatus{
Valid: true,
Tier: TierPro,
Features: append([]string(nil), TierFeatures[TierPro]...),
ExpiresAt: &expiresAt,
}
payload := BuildEntitlementPayload(status, string(SubStateTrial))
if payload.SubscriptionState != string(SubStateTrial) {
t.Fatalf("expected subscription_state %q, got %q", SubStateTrial, payload.SubscriptionState)
}
if payload.TrialExpiresAt == nil {
t.Fatalf("expected trial_expires_at to be populated for trial state")
}
if payload.TrialDaysRemaining == nil {
t.Fatalf("expected trial_days_remaining to be populated for trial state")
}
if *payload.TrialDaysRemaining != 2 {
t.Fatalf("expected trial_days_remaining 2, got %d", *payload.TrialDaysRemaining)
}
}
func TestBuildEntitlementPayload_CopiesStatusDisplayFields(t *testing.T) {
tests := []struct {
name string
planVersion string
}{
{name: "lifetime grandfathered", planVersion: "v5_lifetime_grandfathered"},
{name: "monthly grandfathered", planVersion: "v5_pro_monthly_grandfathered"},
{name: "annual grandfathered", planVersion: "v5_pro_annual_grandfathered"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
expiresAt := time.Now().Add(72 * time.Hour).UTC().Format(time.RFC3339)
graceEnd := time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339)
status := &LicenseStatus{
Valid: true,
Tier: TierPro,
PlanVersion: tc.planVersion,
Email: "owner@example.com",
ExpiresAt: &expiresAt,
IsLifetime: false,
DaysRemaining: 3,
InGracePeriod: true,
GracePeriodEnd: &graceEnd,
Features: append([]string(nil), TierFeatures[TierPro]...),
}
payload := BuildEntitlementPayload(status, string(SubStateGrace))
if !payload.Valid {
t.Fatalf("expected valid=true, got %v", payload.Valid)
}
if payload.PlanVersion != tc.planVersion {
t.Fatalf("expected plan_version %q, got %q", tc.planVersion, payload.PlanVersion)
}
if payload.LicensedEmail != "owner@example.com" {
t.Fatalf("expected licensed_email %q, got %q", "owner@example.com", payload.LicensedEmail)
}
if payload.ExpiresAt == nil || *payload.ExpiresAt != expiresAt {
t.Fatalf("expected expires_at %q, got %v", expiresAt, payload.ExpiresAt)
}
if payload.DaysRemaining != 3 {
t.Fatalf("expected days_remaining 3, got %d", payload.DaysRemaining)
}
if !payload.InGracePeriod {
t.Fatalf("expected in_grace_period=true, got %v", payload.InGracePeriod)
}
if payload.GracePeriodEnd == nil || *payload.GracePeriodEnd != graceEnd {
t.Fatalf("expected grace_period_end %q, got %v", graceEnd, payload.GracePeriodEnd)
}
})
}
}
func TestBuildEntitlementPayload_LifetimeOmitsCommercialCaps(t *testing.T) {
payload := BuildEntitlementPayloadWithUsage(&LicenseStatus{
Valid: true,
Tier: TierLifetime,
PlanVersion: "v5_lifetime_grandfathered",
IsLifetime: true,
Features: append([]string(nil), TierFeatures[TierLifetime]...),
MaxGuests: 0,
}, string(SubStateActive), EntitlementUsageSnapshot{
MonitoredSystems: 15,
MonitoredSystemsAvailable: true,
Guests: 4,
}, nil)
if len(payload.Limits) != 0 {
t.Fatalf("expected lifetime entitlements to omit commercial caps, got %+v", payload.Limits)
}
}
func TestBuildEntitlementPayload_MaxHistoryDays(t *testing.T) {
tests := []struct {
name string
tier Tier
wantDays int
}{
{"free tier gets 7 days", TierFree, 7},
{"relay tier gets 14 days", TierRelay, 14},
{"pro tier gets 90 days", TierPro, 90},
{"pro_plus tier gets 90 days", TierProPlus, 90},
{"pro_annual tier gets 90 days", TierProAnnual, 90},
{"lifetime tier gets 90 days", TierLifetime, 90},
{"cloud tier gets 90 days", TierCloud, 90},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: tc.tier,
Features: append([]string(nil), TierFeatures[tc.tier]...),
}
payload := BuildEntitlementPayload(status, "")
if payload.MaxHistoryDays != tc.wantDays {
t.Fatalf("expected MaxHistoryDays=%d for tier %q, got %d", tc.wantDays, tc.tier, payload.MaxHistoryDays)
}
})
}
}
func TestBuildEntitlementPayload_NilStatus_MaxHistoryDays(t *testing.T) {
payload := BuildEntitlementPayloadWithUsage(nil, "", EntitlementUsageSnapshot{}, nil)
if payload.MaxHistoryDays != TierHistoryDays[TierFree] {
t.Fatalf("expected MaxHistoryDays=%d for nil status, got %d", TierHistoryDays[TierFree], payload.MaxHistoryDays)
}
if payload.HasMigrationGap {
t.Fatal("expected has_migration_gap=false for nil status payload")
}
if payload.LegacyConnections.Total() != 0 {
t.Fatalf("expected zero legacy connections for nil status payload, got %+v", payload.LegacyConnections)
}
}
func TestBuildCommercialPosturePayloadWithUsage_CurrentValues(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierCloud,
PlanVersion: "cloud_starter",
Features: append([]string(nil), TierFeatures[TierPro]...),
}
payload := BuildCommercialPosturePayloadWithUsage(status, "", EntitlementUsageSnapshot{
MonitoredSystems: 7,
MonitoredSystemsAvailable: true,
LegacyConnections: LegacyConnectionCounts{
ProxmoxNodes: 2,
DockerHosts: 1,
},
}, nil)
if payload.Tier != string(TierCloud) {
t.Fatalf("expected tier=%q, got %q", TierCloud, payload.Tier)
}
if payload.SubscriptionState != string(SubStateActive) {
t.Fatalf("expected subscription_state=%q, got %q", SubStateActive, payload.SubscriptionState)
}
if len(payload.UpgradeReasons) != 0 {
t.Fatalf("expected hosted paid posture to omit upgrade reasons, got %v", payload.UpgradeReasons)
}
if payload.LegacyConnections.ProxmoxNodes != 2 || payload.LegacyConnections.DockerHosts != 1 {
t.Fatalf("expected legacy connection counts to be preserved, got %+v", payload.LegacyConnections)
}
if payload.HasMigrationGap {
t.Fatal("expected has_migration_gap=false")
}
}
func TestCommercialPosturePayloadFromEntitlementPayload_StripsBillingIdentityFields(t *testing.T) {
expiresAt := time.Now().Add(48 * time.Hour).UTC().Format(time.RFC3339)
payload := BuildEntitlementPayloadWithUsage(&LicenseStatus{
Valid: true,
Tier: TierCloud,
PlanVersion: "cloud_starter",
Email: "owner@example.com",
ExpiresAt: &expiresAt,
IsLifetime: false,
DaysRemaining: 2,
Features: append([]string(nil), TierFeatures[TierPro]...),
}, string(SubStateActive), EntitlementUsageSnapshot{
MonitoredSystems: 12,
MonitoredSystemsAvailable: true,
LegacyConnections: LegacyConnectionCounts{
ProxmoxNodes: 1,
},
}, nil)
posture := CommercialPosturePayloadFromEntitlementPayload(payload)
if posture.Tier != string(TierCloud) {
t.Fatalf("expected tier=%q, got %q", TierCloud, posture.Tier)
}
if posture.SubscriptionState != string(SubStateActive) {
t.Fatalf("expected subscription_state=%q, got %q", SubStateActive, posture.SubscriptionState)
}
if posture.LegacyConnections.ProxmoxNodes != 1 {
t.Fatalf("expected proxmox_nodes=1, got %+v", posture.LegacyConnections)
}
body, err := json.Marshal(posture)
if err != nil {
t.Fatalf("marshal commercial posture: %v", err)
}
jsonBody := string(body)
for _, forbidden := range []string{
`"capabilities"`,
`"limits"`,
`"licensed_email"`,
`"plan_version"`,
`"hosted_mode"`,
`"valid"`,
`"max_history_days"`,
} {
if strings.Contains(jsonBody, forbidden) {
t.Fatalf("commercial posture leaked billing/runtime field %s in %s", forbidden, jsonBody)
}
}
}
func TestBuildRuntimeCapabilitiesPayloadWithUsage_CurrentValues(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierCloud,
PlanVersion: "cloud_starter",
Features: append([]string(nil), TierFeatures[TierRelay]...),
MaxGuests: 12,
}
payload := BuildRuntimeCapabilitiesPayloadWithUsage(status, "", EntitlementUsageSnapshot{
Guests: 5,
})
if !reflect.DeepEqual(payload.Capabilities, status.Features) {
t.Fatalf("expected capabilities to match status features")
}
if payload.MaxHistoryDays != TierHistoryDays[TierCloud] {
t.Fatalf("expected cloud max history days %d, got %d", TierHistoryDays[TierCloud], payload.MaxHistoryDays)
}
if len(payload.Limits) != 1 {
t.Fatalf("expected one runtime limit, got %d", len(payload.Limits))
}
if payload.Limits[0].Key != "max_guests" {
t.Fatalf("expected runtime limit key max_guests, got %q", payload.Limits[0].Key)
}
if payload.Limits[0].Current != 5 {
t.Fatalf("expected runtime current 5, got %d", payload.Limits[0].Current)
}
}
func TestBuildRuntimeCapabilitiesPayloadWithUsage_StripsInternalOnlyCapabilities(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierEnterprise,
Features: []string{
FeatureRelay,
FeatureDemoFixtures,
},
}
payload := BuildRuntimeCapabilitiesPayloadWithUsage(status, "", EntitlementUsageSnapshot{})
if containsString(payload.Capabilities, FeatureDemoFixtures) {
t.Fatalf("runtime capabilities leaked internal feature %q: %v", FeatureDemoFixtures, payload.Capabilities)
}
if !containsString(payload.Capabilities, FeatureRelay) {
t.Fatalf("runtime capabilities lost public feature %q: %v", FeatureRelay, payload.Capabilities)
}
}
func TestFilterCapabilitiesForRuntimeIdentity_BlocksPrivateRuntimeCapabilitiesOnCommunityBuild(t *testing.T) {
filtered, blocked := FilterCapabilitiesForRuntimeIdentity([]string{
FeatureRelay,
FeatureAuditLogging,
FeatureRBAC,
FeatureAIAutoFix,
FeatureAdvancedReporting,
}, CommunityRuntimeIdentity())
for _, feature := range []string{FeatureAuditLogging, FeatureRBAC, FeatureAIAutoFix} {
if containsString(filtered, feature) {
t.Fatalf("community runtime capabilities retained private feature %q: %v", feature, filtered)
}
}
for _, feature := range []string{FeatureRelay, FeatureAdvancedReporting} {
if !containsString(filtered, feature) {
t.Fatalf("community runtime capabilities lost supported feature %q: %v", feature, filtered)
}
}
if len(blocked) != 3 {
t.Fatalf("blocked capability count=%d, want 3: %+v", len(blocked), blocked)
}
for _, block := range blocked {
if block.Reason != "paid_runtime_required" {
t.Fatalf("blocked reason=%q, want paid_runtime_required", block.Reason)
}
if block.ActionURL != PulseProDownloadURL {
t.Fatalf("blocked action_url=%q, want %q", block.ActionURL, PulseProDownloadURL)
}
}
}
func TestFilterCapabilitiesForRuntimeIdentity_PreservesPrivateRuntimeCapabilitiesOnProBuild(t *testing.T) {
capabilities := []string{FeatureRelay, FeatureAuditLogging, FeatureRBAC, FeatureAIAutoFix}
filtered, blocked := FilterCapabilitiesForRuntimeIdentity(capabilities, ProRuntimeIdentity())
if !reflect.DeepEqual(filtered, capabilities) {
t.Fatalf("pro runtime capabilities=%v, want %v", filtered, capabilities)
}
if len(blocked) != 0 {
t.Fatalf("pro runtime blocked capabilities=%+v, want none", blocked)
}
}
func TestBuildRuntimeCapabilitiesPayload_NilStatusDefaultsToFreeTier(t *testing.T) {
payload := BuildRuntimeCapabilitiesPayload(nil, "")
if payload.MaxHistoryDays != TierHistoryDays[TierFree] {
t.Fatalf("expected MaxHistoryDays=%d for nil status, got %d", TierHistoryDays[TierFree], payload.MaxHistoryDays)
}
if len(payload.Capabilities) != 0 {
t.Fatalf("expected no runtime capabilities for nil status, got %+v", payload.Capabilities)
}
}
func TestBuildRuntimeCapabilitiesPayload_EmptyCollectionsRemainJSONArrays(t *testing.T) {
payload := BuildRuntimeCapabilitiesPayloadWithUsage(&LicenseStatus{
Valid: true,
Tier: TierFree,
Features: []string{},
}, "", EntitlementUsageSnapshot{})
if payload.Capabilities == nil {
t.Fatal("expected runtime capabilities to preserve empty capabilities as []")
}
if payload.Limits == nil {
t.Fatal("expected runtime capabilities to preserve empty limits as []")
}
body, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal runtime capabilities: %v", err)
}
if strings.Contains(string(body), `"capabilities":null`) {
t.Fatalf("runtime capabilities encoded null capabilities: %s", string(body))
}
if strings.Contains(string(body), `"limits":null`) {
t.Fatalf("runtime capabilities encoded null limits: %s", string(body))
}
}
func TestLimitState(t *testing.T) {
tests := []struct {
name string
current int64
limit int64
want string
}{
{name: "ok_below_threshold", current: 50, limit: 100, want: "ok"},
{name: "warning_at_90_percent", current: 90, limit: 100, want: "warning"},
{name: "enforced_at_limit", current: 100, limit: 100, want: "enforced"},
{name: "enforced_above_limit", current: 110, limit: 100, want: "enforced"},
{name: "ok_unlimited", current: 50, limit: 0, want: "ok"},
{name: "small_limit_ok", current: 3, limit: 5, want: "ok"},
{name: "small_limit_warning_at_n_minus_1", current: 4, limit: 5, want: "warning"},
{name: "small_limit_enforced_at_limit", current: 5, limit: 5, want: "enforced"},
{name: "small_limit_8_warning", current: 7, limit: 8, want: "warning"},
{name: "small_limit_8_ok", current: 6, limit: 8, want: "ok"},
{name: "small_limit_10_warning", current: 9, limit: 10, want: "warning"},
{name: "small_limit_10_ok", current: 8, limit: 10, want: "ok"},
{name: "limit_1_zero_usage_ok", current: 0, limit: 1, want: "ok"},
{name: "limit_1_at_limit_enforced", current: 1, limit: 1, want: "enforced"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := LimitState(tc.current, tc.limit)
if got != tc.want {
t.Fatalf("LimitState(%d, %d) = %q, want %q", tc.current, tc.limit, got, tc.want)
}
})
}
}