Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
This commit is contained in:
327
pkg/license/enforce_test.go
Normal file
327
pkg/license/enforce_test.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package license
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// setupTestStore creates a temporary license store for testing.
|
||||
func setupTestStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
return &Store{Dir: dir}
|
||||
}
|
||||
|
||||
// saveLicense writes a license to the test store.
|
||||
func saveLicense(t *testing.T, store *Store, lic *License) {
|
||||
t.Helper()
|
||||
data, err := yaml.Marshal(lic)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal license: %v", err)
|
||||
}
|
||||
if err := os.MkdirAll(store.Dir, 0700); err != nil {
|
||||
t.Fatalf("failed to create store dir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(store.Dir, "license.yaml"), data, 0600); err != nil {
|
||||
t.Fatalf("failed to write license: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequireFeature_CommunityAllowed verifies that Community-tier features
|
||||
// (like CAS) are allowed without any license.
|
||||
func TestRequireFeature_CommunityAllowed(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
// No license file — defaults to Community tier
|
||||
|
||||
communityFeatures := []string{"cas", "containers", "networking-basic", "security-profiles", "logs", "ps", "cas-pull", "cas-push"}
|
||||
for _, feature := range communityFeatures {
|
||||
err := RequireFeatureWithStore(store, feature)
|
||||
if err != nil {
|
||||
t.Errorf("Community feature %q should be allowed without license, got: %v", feature, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequireFeature_ProDeniedWithoutLicense verifies that Pro-tier features
|
||||
// (like VMs) are denied without a license.
|
||||
func TestRequireFeature_ProDeniedWithoutLicense(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
// No license file
|
||||
|
||||
proFeatures := []string{"vms", "cas-distributed", "cluster", "cicada"}
|
||||
for _, feature := range proFeatures {
|
||||
err := RequireFeatureWithStore(store, feature)
|
||||
if err == nil {
|
||||
t.Errorf("Pro feature %q should be DENIED without license", feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequireFeature_ProAllowedWithProLicense verifies that Pro features
|
||||
// work with a Pro license.
|
||||
func TestRequireFeature_ProAllowedWithProLicense(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
saveLicense(t, store, &License{
|
||||
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
|
||||
Tier: TierPro,
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
proFeatures := []string{"vms", "cas-distributed", "cluster", "cicada", "cas", "containers"}
|
||||
for _, feature := range proFeatures {
|
||||
err := RequireFeatureWithStore(store, feature)
|
||||
if err != nil {
|
||||
t.Errorf("Pro feature %q should be allowed with Pro license, got: %v", feature, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequireFeature_EnterpriseDeniedWithProLicense verifies that Enterprise
|
||||
// features are denied with only a Pro license.
|
||||
func TestRequireFeature_EnterpriseDeniedWithProLicense(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
saveLicense(t, store, &License{
|
||||
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
|
||||
Tier: TierPro,
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
enterpriseFeatures := []string{"sso", "rbac", "audit", "live-migration", "cas-cross-region"}
|
||||
for _, feature := range enterpriseFeatures {
|
||||
err := RequireFeatureWithStore(store, feature)
|
||||
if err == nil {
|
||||
t.Errorf("Enterprise feature %q should be DENIED with Pro license", feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequireFeature_EnterpriseAllowed verifies Enterprise features with
|
||||
// an Enterprise license.
|
||||
func TestRequireFeature_EnterpriseAllowed(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
saveLicense(t, store, &License{
|
||||
Key: "VOLT-ENT-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
|
||||
Tier: TierEnterprise,
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
features := []string{"sso", "rbac", "vms", "cas", "containers", "live-migration"}
|
||||
for _, feature := range features {
|
||||
err := RequireFeatureWithStore(store, feature)
|
||||
if err != nil {
|
||||
t.Errorf("Feature %q should be allowed with Enterprise license, got: %v", feature, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequireContainerLimit verifies container limit enforcement by tier.
|
||||
func TestRequireContainerLimit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tier string
|
||||
count int
|
||||
wantError bool
|
||||
}{
|
||||
{"Community under limit", TierCommunity, 25, false},
|
||||
{"Community at limit", TierCommunity, 50, true},
|
||||
{"Community over limit", TierCommunity, 75, true},
|
||||
{"Pro under limit", TierPro, 250, false},
|
||||
{"Pro at limit", TierPro, 500, true},
|
||||
{"Pro over limit", TierPro, 750, true},
|
||||
{"Enterprise unlimited", TierEnterprise, 99999, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
|
||||
if tt.tier != TierCommunity {
|
||||
saveLicense(t, store, &License{
|
||||
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
|
||||
Tier: tt.tier,
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
err := RequireContainerLimitWithStore(store, tt.count)
|
||||
if tt.wantError && err == nil {
|
||||
t.Errorf("expected error for %d containers on %s tier", tt.count, tt.tier)
|
||||
}
|
||||
if !tt.wantError && err != nil {
|
||||
t.Errorf("expected no error for %d containers on %s tier, got: %v", tt.count, tt.tier, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequireContainerLimit_NoLicense verifies container limits with no license (Community).
|
||||
func TestRequireContainerLimit_NoLicense(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
|
||||
err := RequireContainerLimitWithStore(store, 25)
|
||||
if err != nil {
|
||||
t.Errorf("25 containers should be within Community limit, got: %v", err)
|
||||
}
|
||||
|
||||
err = RequireContainerLimitWithStore(store, 50)
|
||||
if err == nil {
|
||||
t.Error("50 containers should exceed Community limit")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrialExpiration verifies that expired trials fall back to Community.
|
||||
func TestTrialExpiration(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
|
||||
// Active trial — Pro features should work
|
||||
saveLicense(t, store, &License{
|
||||
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
|
||||
Tier: TierPro,
|
||||
IsTrial: true,
|
||||
TrialEndsAt: time.Now().Add(24 * time.Hour), // expires tomorrow
|
||||
CouponCode: "TEST2025",
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
err := RequireFeatureWithStore(store, "vms")
|
||||
if err != nil {
|
||||
t.Errorf("Active trial should allow Pro features, got: %v", err)
|
||||
}
|
||||
|
||||
// Expired trial — Pro features should be denied
|
||||
saveLicense(t, store, &License{
|
||||
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
|
||||
Tier: TierPro,
|
||||
IsTrial: true,
|
||||
TrialEndsAt: time.Now().Add(-24 * time.Hour), // expired yesterday
|
||||
CouponCode: "TEST2025",
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
err = RequireFeatureWithStore(store, "vms")
|
||||
if err == nil {
|
||||
t.Error("Expired trial should DENY Pro features")
|
||||
}
|
||||
|
||||
// Expired trial — Community features should still work
|
||||
err = RequireFeatureWithStore(store, "cas")
|
||||
if err != nil {
|
||||
t.Errorf("Expired trial should still allow Community features, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrialExpiration_ContainerLimit verifies expired trials use Community container limits.
|
||||
func TestTrialExpiration_ContainerLimit(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
|
||||
// Expired trial
|
||||
saveLicense(t, store, &License{
|
||||
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
|
||||
Tier: TierPro,
|
||||
IsTrial: true,
|
||||
TrialEndsAt: time.Now().Add(-1 * time.Hour),
|
||||
ActivatedAt: time.Now(),
|
||||
})
|
||||
|
||||
// Should use Community limit (50), not Pro limit (500)
|
||||
err := RequireContainerLimitWithStore(store, 50)
|
||||
if err == nil {
|
||||
t.Error("Expired trial should use Community container limit (50)")
|
||||
}
|
||||
|
||||
err = RequireContainerLimitWithStore(store, 25)
|
||||
if err != nil {
|
||||
t.Errorf("25 containers should be within Community limit even with expired trial, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsTrialExpired verifies the License.IsTrialExpired() method.
|
||||
func TestIsTrialExpired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
license License
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "not a trial",
|
||||
license: License{IsTrial: false},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "trial with zero expiry",
|
||||
license: License{IsTrial: true},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "active trial",
|
||||
license: License{IsTrial: true, TrialEndsAt: time.Now().Add(24 * time.Hour)},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "expired trial",
|
||||
license: License{IsTrial: true, TrialEndsAt: time.Now().Add(-24 * time.Hour)},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.license.IsTrialExpired()
|
||||
if got != tt.expected {
|
||||
t.Errorf("IsTrialExpired() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequiredTier verifies the requiredTier helper returns the minimum tier.
|
||||
func TestRequiredTier(t *testing.T) {
|
||||
tests := []struct {
|
||||
feature string
|
||||
expected string
|
||||
}{
|
||||
{"cas", "Community"},
|
||||
{"containers", "Community"},
|
||||
{"vms", "Professional"},
|
||||
{"cluster", "Professional"},
|
||||
{"sso", "Enterprise"},
|
||||
{"rbac", "Enterprise"},
|
||||
{"nonexistent", "Unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.feature, func(t *testing.T) {
|
||||
got := requiredTier(tt.feature)
|
||||
if got != tt.expected {
|
||||
t.Errorf("requiredTier(%q) = %q, want %q", tt.feature, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequireFeature_ExpiredLicense verifies expired non-trial licenses.
|
||||
func TestRequireFeature_ExpiredLicense(t *testing.T) {
|
||||
store := setupTestStore(t)
|
||||
saveLicense(t, store, &License{
|
||||
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
|
||||
Tier: TierPro,
|
||||
ActivatedAt: time.Now().Add(-365 * 24 * time.Hour),
|
||||
ExpiresAt: time.Now().Add(-24 * time.Hour), // expired yesterday
|
||||
})
|
||||
|
||||
// Pro feature should be denied
|
||||
err := RequireFeatureWithStore(store, "vms")
|
||||
if err == nil {
|
||||
t.Error("Expired license should deny Pro features")
|
||||
}
|
||||
|
||||
// Community feature should still work
|
||||
err = RequireFeatureWithStore(store, "cas")
|
||||
if err != nil {
|
||||
t.Errorf("Expired license should still allow Community features, got: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user