From 3badd0a840d7e329d55c92a0fc2a2ea707c9ade5 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 10 Jun 2026 17:09:00 +0100 Subject: [PATCH] fix(licensing): honor activated licenses for dev-grant-excluded features in dev mode In dev/demo builds HasFeature returned the implicit dev grant verdict unconditionally, so features excluded from that grant (white_label, multi_user, unlimited, env-gated multi_tenant) were denied even when an explicitly activated, signature-verified license carried them - while /api/license/status reported them active from the same claims. That inconsistency made branded report rendering impossible to exercise in dev builds. Excluded features now fall through to real entitlement evaluation, so an activated license behaves the same in dev as in release builds; the no-license dev posture is unchanged. --- pkg/licensing/service.go | 8 ++++++- pkg/licensing/service_activate_test.go | 32 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pkg/licensing/service.go b/pkg/licensing/service.go index 70c453200..7bfcc5380 100644 --- a/pkg/licensing/service.go +++ b/pkg/licensing/service.go @@ -535,7 +535,13 @@ func (s *Service) IsValid() bool { func (s *Service) HasFeature(feature string) bool { // In demo mode or dev mode, grant all Pro features if isDemoMode() || isDevMode() { - return devModeFeatureEnabled(feature) + if devModeFeatureEnabled(feature) { + return true + } + // Features excluded from the implicit dev grant (white_label, + // multi_user, unlimited, env-gated multi_tenant) fall through to + // real entitlement evaluation, so an explicitly activated license + // behaves the same in dev builds as in release builds. } s.mu.Lock() // Need write lock since we may update grace period diff --git a/pkg/licensing/service_activate_test.go b/pkg/licensing/service_activate_test.go index dc2be06d0..e791cb6d8 100644 --- a/pkg/licensing/service_activate_test.go +++ b/pkg/licensing/service_activate_test.go @@ -567,6 +567,38 @@ func TestServiceStatus_DevModeKeepsCustomerFacingStatusCommunityWithoutLicense(t } } +func TestServiceHasFeature_DevModeHonorsActivatedLicenseForExcludedFeatures(t *testing.T) { + t.Setenv("PULSE_DEV", "true") + t.Setenv("PULSE_MOCK_MODE", "true") + t.Setenv("PULSE_LICENSE_DEV_MODE", "true") + t.Setenv("PULSE_MULTI_TENANT_ENABLED", "") + SetPublicKey(nil) + t.Cleanup(func() { SetPublicKey(nil) }) + + svc := NewService() + if svc.HasFeature(FeatureWhiteLabel) { + t.Fatalf("HasFeature(%q)=true without a license, want false in dev mode", FeatureWhiteLabel) + } + + licenseKey, err := GenerateLicenseForTesting("dev-white-label@example.com", TierEnterprise, 24*time.Hour) + if err != nil { + t.Fatalf("GenerateLicenseForTesting: %v", err) + } + if _, err := svc.Activate(licenseKey); err != nil { + t.Fatalf("Activate in dev mode: %v", err) + } + + // The implicit dev grant excludes white_label, but an explicitly + // activated license that carries it must behave like a release build. + if !svc.HasFeature(FeatureWhiteLabel) { + t.Fatalf("HasFeature(%q)=false with an activated enterprise license, want true", FeatureWhiteLabel) + } + // Features the dev grant already enables stay enabled. + if !svc.HasFeature(FeatureAdvancedReporting) { + t.Fatalf("HasFeature(%q)=false, want true for dev-mode backend gate bypass", FeatureAdvancedReporting) + } +} + func TestServiceStatus_DevModeMultiTenantBypassDoesNotChangeCustomerFacingStatus(t *testing.T) { t.Setenv("PULSE_DEV", "true") t.Setenv("PULSE_MULTI_TENANT_ENABLED", "true")