mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 08:57:12 +00:00
332 lines
9.8 KiB
Go
332 lines
9.8 KiB
Go
package licensing
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestPersistence(t *testing.T) {
|
|
// Create a temporary directory for config
|
|
tmpDir, err := os.MkdirTemp("", "pulse-license-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
p, err := NewPersistence(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create persistence: %v", err)
|
|
}
|
|
|
|
testLicenseKey := "test-license-key-123"
|
|
|
|
t.Run("Save and Load", func(t *testing.T) {
|
|
err := p.Save(testLicenseKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to save license: %v", err)
|
|
}
|
|
|
|
if !p.Exists() {
|
|
t.Error("License file should exist")
|
|
}
|
|
|
|
loadedKey, err := p.Load()
|
|
if err != nil {
|
|
t.Fatalf("Failed to load license: %v", err)
|
|
}
|
|
|
|
if loadedKey != testLicenseKey {
|
|
t.Errorf("Expected license key %s, got %s", testLicenseKey, loadedKey)
|
|
}
|
|
})
|
|
|
|
t.Run("Save and Load with Grace Period", func(t *testing.T) {
|
|
gracePeriodEnd := time.Now().Add(7 * 24 * time.Hour).Unix()
|
|
err := p.SaveWithGracePeriod(testLicenseKey, &gracePeriodEnd)
|
|
if err != nil {
|
|
t.Fatalf("Failed to save license with grace period: %v", err)
|
|
}
|
|
|
|
persisted, err := p.LoadWithMetadata()
|
|
if err != nil {
|
|
t.Fatalf("Failed to load license with metadata: %v", err)
|
|
}
|
|
|
|
if persisted.LicenseKey != testLicenseKey {
|
|
t.Errorf("Expected license key %s, got %s", testLicenseKey, persisted.LicenseKey)
|
|
}
|
|
|
|
if persisted.GracePeriodEnd == nil || *persisted.GracePeriodEnd != gracePeriodEnd {
|
|
t.Errorf("Expected grace period end %v, got %v", gracePeriodEnd, persisted.GracePeriodEnd)
|
|
}
|
|
})
|
|
|
|
t.Run("Load non-existent", func(t *testing.T) {
|
|
tmpDirEmpty, _ := os.MkdirTemp("", "pulse-license-test-empty-*")
|
|
defer os.RemoveAll(tmpDirEmpty)
|
|
|
|
pEmpty, _ := NewPersistence(tmpDirEmpty)
|
|
key, err := pEmpty.Load()
|
|
if err != nil {
|
|
t.Fatalf("Expected no error for non-existent license, got %v", err)
|
|
}
|
|
if key != "" {
|
|
t.Errorf("Expected empty key, got %s", key)
|
|
}
|
|
})
|
|
|
|
t.Run("Delete", func(t *testing.T) {
|
|
err := p.Save(testLicenseKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to save license: %v", err)
|
|
}
|
|
|
|
if !p.Exists() {
|
|
t.Fatal("License should exist before delete")
|
|
}
|
|
|
|
err = p.Delete()
|
|
if err != nil {
|
|
t.Fatalf("Failed to delete license: %v", err)
|
|
}
|
|
|
|
if p.Exists() {
|
|
t.Error("License should not exist after delete")
|
|
}
|
|
})
|
|
|
|
t.Run("Encryption check", func(t *testing.T) {
|
|
err := p.Save(testLicenseKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to save license: %v", err)
|
|
}
|
|
|
|
// Read the file directly - it should be base64 encoded and encrypted
|
|
licensePath := filepath.Join(tmpDir, LicenseFileName)
|
|
data, err := os.ReadFile(licensePath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read license file: %v", err)
|
|
}
|
|
|
|
if string(data) == testLicenseKey {
|
|
t.Error("License file should be encrypted, not raw text")
|
|
}
|
|
|
|
// Ensure it's not JSON either in raw form
|
|
if data[0] == '{' {
|
|
t.Error("License file should be encrypted, not raw JSON")
|
|
}
|
|
})
|
|
|
|
t.Run("Decrypt with wrong key material", func(t *testing.T) {
|
|
err := p.Save(testLicenseKey)
|
|
if err != nil {
|
|
t.Fatalf("Failed to save license: %v", err)
|
|
}
|
|
|
|
// Create a new persistence with different encryption key
|
|
pWrong := &Persistence{
|
|
configDir: tmpDir,
|
|
encryptionKey: "different-encryption-key",
|
|
machineID: "different-machine-id",
|
|
}
|
|
|
|
_, err = pWrong.Load()
|
|
if err == nil {
|
|
t.Error("Expected error when decrypting with wrong key material")
|
|
}
|
|
})
|
|
|
|
t.Run("Backwards compatibility with machine-id", func(t *testing.T) {
|
|
// This tests the scenario where a license was saved with machine-id
|
|
// (old behavior before persistent key feature) and we're now loading it
|
|
// with a different primary key but same machine-id as fallback
|
|
|
|
tmpDirCompat, _ := os.MkdirTemp("", "pulse-license-compat-*")
|
|
defer os.RemoveAll(tmpDirCompat)
|
|
|
|
testKey := "compat-test-key"
|
|
machineID := "test-machine-id-12345"
|
|
|
|
// Simulate old behavior: directly encrypt with machine-id (without calling Save
|
|
// which would create a persistent key). This is what old installations have.
|
|
pOld := &Persistence{
|
|
configDir: tmpDirCompat,
|
|
encryptionKey: machineID, // Old behavior used machine-id as encryption key
|
|
machineID: machineID,
|
|
}
|
|
|
|
// Manually encrypt and save without creating persistent key
|
|
persisted := PersistedLicense{LicenseKey: testKey}
|
|
jsonData, _ := json.Marshal(persisted)
|
|
encrypted, err := pOld.encrypt(jsonData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to encrypt license: %v", err)
|
|
}
|
|
|
|
// Write directly to simulate old installation
|
|
_ = os.MkdirAll(tmpDirCompat, 0700)
|
|
licensePath := filepath.Join(tmpDirCompat, LicenseFileName)
|
|
encoded := base64.StdEncoding.EncodeToString(encrypted)
|
|
_ = os.WriteFile(licensePath, []byte(encoded), 0600)
|
|
|
|
// Verify no persistent key file exists (simulating old installation)
|
|
keyPath := filepath.Join(tmpDirCompat, PersistentKeyFileName)
|
|
if _, err := os.Stat(keyPath); err == nil {
|
|
t.Fatal("Persistent key should not exist for this test")
|
|
}
|
|
|
|
// Now try to load with a new primary key but same machine-id as fallback
|
|
// This simulates what happens when a Docker user upgrades and gets a
|
|
// persistent key file, but their old license was encrypted with machine-id
|
|
pNew := &Persistence{
|
|
configDir: tmpDirCompat,
|
|
encryptionKey: "new-persistent-key", // Different primary key (simulating new container)
|
|
machineID: machineID, // Same machine-id for fallback
|
|
}
|
|
|
|
loaded, err := pNew.LoadWithMetadata()
|
|
if err != nil {
|
|
t.Fatalf("Failed to load license with machine-id fallback: %v\n"+
|
|
"This means backwards compatibility is broken for existing Docker users", err)
|
|
}
|
|
|
|
if loaded.LicenseKey != testKey {
|
|
t.Errorf("Expected license key %s, got %s", testKey, loaded.LicenseKey)
|
|
}
|
|
})
|
|
|
|
t.Run("plaintext license file rewrites encrypted storage on load", func(t *testing.T) {
|
|
plainDir, err := os.MkdirTemp("", "pulse-license-plaintext-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(plainDir)
|
|
|
|
pPlain, err := NewPersistence(plainDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create persistence: %v", err)
|
|
}
|
|
|
|
gracePeriodEnd := time.Now().Add(24 * time.Hour).Unix()
|
|
plaintext := PersistedLicense{
|
|
LicenseKey: "pit-license-plaintext-test",
|
|
GracePeriodEnd: &gracePeriodEnd,
|
|
}
|
|
|
|
raw, err := json.Marshal(plaintext)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal plaintext license: %v", err)
|
|
}
|
|
|
|
licensePath := filepath.Join(plainDir, LicenseFileName)
|
|
if err := os.WriteFile(licensePath, raw, 0600); err != nil {
|
|
t.Fatalf("Failed to write plaintext license file: %v", err)
|
|
}
|
|
|
|
loaded, err := pPlain.LoadWithMetadata()
|
|
if err != nil {
|
|
t.Fatalf("LoadWithMetadata plaintext migration failed: %v", err)
|
|
}
|
|
|
|
if loaded.LicenseKey != plaintext.LicenseKey {
|
|
t.Fatalf("LicenseKey = %q, want %q", loaded.LicenseKey, plaintext.LicenseKey)
|
|
}
|
|
if loaded.GracePeriodEnd == nil || *loaded.GracePeriodEnd != gracePeriodEnd {
|
|
t.Fatalf("GracePeriodEnd = %v, want %v", loaded.GracePeriodEnd, gracePeriodEnd)
|
|
}
|
|
|
|
rewritten, err := os.ReadFile(licensePath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read rewritten license file: %v", err)
|
|
}
|
|
if bytes.Equal(rewritten, raw) {
|
|
t.Fatal("expected plaintext license file to be rewritten encrypted")
|
|
}
|
|
if bytes.Contains(rewritten, []byte(plaintext.LicenseKey)) {
|
|
t.Fatal("license key should not remain in plaintext after migration rewrite")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPersistenceEnforcesOwnerOnlyPermissions(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
if err := os.Chmod(tmpDir, 0755); err != nil {
|
|
t.Fatalf("failed to relax temp dir perms: %v", err)
|
|
}
|
|
|
|
p, err := NewPersistence(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create persistence: %v", err)
|
|
}
|
|
if err := p.Save("owner-only-perm-test"); err != nil {
|
|
t.Fatalf("Save failed: %v", err)
|
|
}
|
|
|
|
dirInfo, err := os.Stat(tmpDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat config dir: %v", err)
|
|
}
|
|
if got := dirInfo.Mode().Perm(); got != 0700 {
|
|
t.Fatalf("config dir perms = %o, want 700", got)
|
|
}
|
|
|
|
for _, file := range []string{LicenseFileName, PersistentKeyFileName} {
|
|
path := filepath.Join(tmpDir, file)
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("failed to stat %s: %v", file, err)
|
|
}
|
|
if got := info.Mode().Perm(); got != 0600 {
|
|
t.Fatalf("%s perms = %o, want 600", file, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolvePersistencePathCanonicalizesConfigDir(t *testing.T) {
|
|
baseDir := t.TempDir()
|
|
inputDir := " " + filepath.Join(baseDir, "nested", "..", "licensing") + string(os.PathSeparator) + ". "
|
|
|
|
path, err := resolvePersistencePath(inputDir, LicenseFileName)
|
|
if err != nil {
|
|
t.Fatalf("resolvePersistencePath() error = %v", err)
|
|
}
|
|
|
|
wantDir := filepath.Clean(filepath.Join(baseDir, "nested", "..", "licensing") + string(os.PathSeparator) + ".")
|
|
if path != filepath.Join(wantDir, LicenseFileName) {
|
|
t.Fatalf("resolvePersistencePath() = %q, want %q", path, filepath.Join(wantDir, LicenseFileName))
|
|
}
|
|
}
|
|
|
|
func TestResolvePersistencePathRejectsBlankConfigDir(t *testing.T) {
|
|
if _, err := resolvePersistencePath(" \t ", LicenseFileName); err == nil {
|
|
t.Fatal("expected blank config dir to be rejected")
|
|
}
|
|
}
|
|
|
|
func TestNewPersistenceRejectsSymlinkPersistentKey(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
target := filepath.Join(tmpDir, "key-target")
|
|
if err := os.WriteFile(target, []byte("test-key"), 0600); err != nil {
|
|
t.Fatalf("failed to write target file: %v", err)
|
|
}
|
|
|
|
keyPath := filepath.Join(tmpDir, PersistentKeyFileName)
|
|
if err := os.Symlink(target, keyPath); err != nil {
|
|
t.Skipf("symlink unsupported on this platform: %v", err)
|
|
}
|
|
|
|
_, err := NewPersistence(tmpDir)
|
|
if err == nil {
|
|
t.Fatal("expected error for symlink persistent key path")
|
|
}
|
|
if !strings.Contains(err.Error(), "symlink") {
|
|
t.Fatalf("expected symlink error, got: %v", err)
|
|
}
|
|
}
|