mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-05 23:36:37 +00:00
Canonicalize fixed alerts and licensing paths
This commit is contained in:
parent
7dcd564997
commit
f1fc17e627
9 changed files with 220 additions and 37 deletions
|
|
@ -44,6 +44,7 @@ operator-facing alert routing behavior for live runtime alerts.
|
|||
22. `frontend-modern/src/utils/alertTabsPresentation.ts`
|
||||
23. `frontend-modern/src/utils/alertThresholdsPresentation.ts`
|
||||
24. `frontend-modern/src/utils/alertThresholdsSectionPresentation.ts`
|
||||
25. `internal/alerts/history.go`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -54,6 +55,7 @@ operator-facing alert routing behavior for live runtime alerts.
|
|||
1. Add new alert rule kinds in `internal/alerts/specs/`
|
||||
2. Add typed collector/builders in `internal/alerts/alerts.go`
|
||||
3. Add identity/persistence updates through canonical alert helpers only
|
||||
4. Add or change alert history persistence through `internal/alerts/history.go` using normalized owned storage roots and fixed storage leaves only
|
||||
|
||||
## Forbidden Paths
|
||||
|
||||
|
|
@ -74,6 +76,14 @@ Canonical alert identity and evaluation are the live runtime model. Remaining
|
|||
legacy references should exist only in explicit migration or negative test
|
||||
boundaries.
|
||||
|
||||
Alert history persistence is also part of that canonical boundary. The history
|
||||
manager may choose the owned runtime data directory, but it must normalize that
|
||||
directory once and then resolve only the fixed `alert-history.json` and
|
||||
`alert-history.backup.json` leaves through the shared storage-path helper
|
||||
before any filesystem read, write, rename, or delete. Future history-persistence
|
||||
changes must not reintroduce raw `filepath.Join(dataDir, ...)` joins from
|
||||
caller-supplied directories or ad hoc history filenames.
|
||||
|
||||
Notification transport, provider delivery, queue safety, and notification API
|
||||
transport now live under the explicit `notifications` subsystem inside the
|
||||
current architecture lane. The alerts surface still owns operator-facing alert
|
||||
|
|
|
|||
|
|
@ -162,6 +162,14 @@ key and grace-period metadata in runtime state, but a legacy plaintext
|
|||
`license.enc` may only serve as migration input. Once it can be read,
|
||||
canonical persistence must rewrite encrypted storage immediately on load
|
||||
instead of treating plaintext licensing state as a valid steady-state path.
|
||||
That same local-persistence boundary also owns the filesystem path contract for
|
||||
commercial secrets at rest. `pkg/licensing/persistence.go` and
|
||||
`pkg/licensing/activation_store.go` must normalize the owned config directory
|
||||
once and resolve only the fixed `.license-key`, `license.enc`, and
|
||||
`activation.enc` leaves through the shared storage-path helper before any
|
||||
filesystem read, write, rename, stat, or delete. Future licensing persistence
|
||||
changes must not bypass that resolver with raw `filepath.Join(configDir, ...)`
|
||||
joins or introduce caller-controlled persistence filenames.
|
||||
Hosted entitlement-source loading follows the same rule: `DatabaseSource` must
|
||||
normalize persisted Cloud/MSP plan aliases and legacy limit keys before runtime
|
||||
evaluation, but it must not fabricate a canonical `plan_version` from bare
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/securityutil"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
|
@ -67,26 +68,45 @@ func historyIdentityKey(alert *Alert) string {
|
|||
return alert.ID
|
||||
}
|
||||
|
||||
func resolveHistoryStoragePaths(dataDir string) (resolvedDataDir string, historyFile string, backupFile string, err error) {
|
||||
resolvedDataDir, err = securityutil.NormalizeStorageDir(dataDir)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("resolve history data directory: %w", err)
|
||||
}
|
||||
|
||||
historyFile, err = securityutil.JoinStorageLeaf(resolvedDataDir, HistoryFileName)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("resolve history file path: %w", err)
|
||||
}
|
||||
backupFile, err = securityutil.JoinStorageLeaf(resolvedDataDir, HistoryBackupFileName)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("resolve history backup file path: %w", err)
|
||||
}
|
||||
|
||||
return resolvedDataDir, historyFile, backupFile, nil
|
||||
}
|
||||
|
||||
// NewHistoryManager creates a new history manager
|
||||
func NewHistoryManager(dataDir string) *HistoryManager {
|
||||
if dataDir == "" {
|
||||
dataDir = utils.GetDataDir()
|
||||
resolvedDataDir, historyFile, backupFile, err := resolveHistoryStoragePaths(utils.ResolveDataDir(dataDir))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("invalid alert history storage paths for %q: %v", dataDir, err))
|
||||
}
|
||||
|
||||
hm := &HistoryManager{
|
||||
dataDir: dataDir,
|
||||
historyFile: filepath.Join(dataDir, HistoryFileName),
|
||||
backupFile: filepath.Join(dataDir, HistoryBackupFileName),
|
||||
dataDir: resolvedDataDir,
|
||||
historyFile: historyFile,
|
||||
backupFile: backupFile,
|
||||
history: make([]HistoryEntry, 0),
|
||||
saveInterval: 5 * time.Minute,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Ensure data directory exists
|
||||
if err := os.MkdirAll(dataDir, alertsDirPerm); err != nil {
|
||||
log.Error().Err(err).Str("dir", dataDir).Msg("Failed to create data directory")
|
||||
} else if err := os.Chmod(dataDir, alertsDirPerm); err != nil {
|
||||
log.Warn().Err(err).Str("dir", dataDir).Msg("Failed to harden history directory permissions")
|
||||
if err := os.MkdirAll(resolvedDataDir, alertsDirPerm); err != nil {
|
||||
log.Error().Err(err).Str("dir", resolvedDataDir).Msg("Failed to create data directory")
|
||||
} else if err := os.Chmod(resolvedDataDir, alertsDirPerm); err != nil {
|
||||
log.Warn().Err(err).Str("dir", resolvedDataDir).Msg("Failed to harden history directory permissions")
|
||||
}
|
||||
|
||||
// Load existing history
|
||||
|
|
|
|||
|
|
@ -14,12 +14,16 @@ func newTestHistoryManager(t *testing.T) *HistoryManager {
|
|||
t.Helper()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
resolvedDataDir, historyFile, backupFile, err := resolveHistoryStoragePaths(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveHistoryStoragePaths(%q) error = %v", tempDir, err)
|
||||
}
|
||||
|
||||
// Create minimal HistoryManager without starting goroutines
|
||||
hm := &HistoryManager{
|
||||
dataDir: tempDir,
|
||||
historyFile: filepath.Join(tempDir, HistoryFileName),
|
||||
backupFile: filepath.Join(tempDir, HistoryBackupFileName),
|
||||
dataDir: resolvedDataDir,
|
||||
historyFile: historyFile,
|
||||
backupFile: backupFile,
|
||||
history: make([]HistoryEntry, 0),
|
||||
saveInterval: 5 * time.Minute,
|
||||
stopChan: make(chan struct{}),
|
||||
|
|
@ -28,6 +32,33 @@ func newTestHistoryManager(t *testing.T) *HistoryManager {
|
|||
return hm
|
||||
}
|
||||
|
||||
func TestResolveHistoryStoragePathsCanonicalizesDataDir(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
inputDir := " " + filepath.Join(baseDir, "nested", "..", "history") + string(os.PathSeparator) + ". "
|
||||
|
||||
resolvedDataDir, historyFile, backupFile, err := resolveHistoryStoragePaths(inputDir)
|
||||
if err != nil {
|
||||
t.Fatalf("resolveHistoryStoragePaths() error = %v", err)
|
||||
}
|
||||
|
||||
wantDir := filepath.Clean(filepath.Join(baseDir, "nested", "..", "history") + string(os.PathSeparator) + ".")
|
||||
if resolvedDataDir != wantDir {
|
||||
t.Fatalf("resolvedDataDir = %q, want %q", resolvedDataDir, wantDir)
|
||||
}
|
||||
if historyFile != filepath.Join(wantDir, HistoryFileName) {
|
||||
t.Fatalf("historyFile = %q, want %q", historyFile, filepath.Join(wantDir, HistoryFileName))
|
||||
}
|
||||
if backupFile != filepath.Join(wantDir, HistoryBackupFileName) {
|
||||
t.Fatalf("backupFile = %q, want %q", backupFile, filepath.Join(wantDir, HistoryBackupFileName))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveHistoryStoragePathsRejectsBlankDir(t *testing.T) {
|
||||
if _, _, _, err := resolveHistoryStoragePaths(" \t "); err == nil {
|
||||
t.Fatal("expected blank history data dir to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStats_EmptyHistory(t *testing.T) {
|
||||
// t.Parallel()
|
||||
|
||||
|
|
|
|||
|
|
@ -14,13 +14,23 @@ func HashedStorageName(id string) string {
|
|||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
// JoinStorageLeaf joins an already-owned storage directory with a validated leaf filename.
|
||||
func JoinStorageLeaf(dir, leaf string) (string, error) {
|
||||
// NormalizeStorageDir trims and canonicalizes an already-owned storage directory path.
|
||||
func NormalizeStorageDir(dir string) (string, error) {
|
||||
trimmedDir := strings.TrimSpace(dir)
|
||||
if trimmedDir == "" {
|
||||
return "", fmt.Errorf("storage directory is required")
|
||||
}
|
||||
|
||||
return filepath.Clean(trimmedDir), nil
|
||||
}
|
||||
|
||||
// JoinStorageLeaf joins an already-owned storage directory with a validated leaf filename.
|
||||
func JoinStorageLeaf(dir, leaf string) (string, error) {
|
||||
normalizedDir, err := NormalizeStorageDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(leaf)
|
||||
if name == "" {
|
||||
return "", fmt.Errorf("storage leaf is required")
|
||||
|
|
@ -35,5 +45,5 @@ func JoinStorageLeaf(dir, leaf string) (string, error) {
|
|||
return "", fmt.Errorf("storage leaf must not contain path separators")
|
||||
}
|
||||
|
||||
return filepath.Join(trimmedDir, name), nil
|
||||
return filepath.Join(normalizedDir, name), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
package securityutil
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeStorageDir(t *testing.T) {
|
||||
dir, err := NormalizeStorageDir(" /tmp/pulse/../pulse ")
|
||||
if err != nil {
|
||||
t.Fatalf("NormalizeStorageDir() error = %v", err)
|
||||
}
|
||||
if dir != filepath.Clean("/tmp/pulse/../pulse") {
|
||||
t.Fatalf("NormalizeStorageDir() = %q", dir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeStorageDirRejectsBlank(t *testing.T) {
|
||||
if _, err := NormalizeStorageDir(" \t "); err == nil {
|
||||
t.Fatal("expected blank storage dir to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinStorageLeaf(t *testing.T) {
|
||||
path, err := JoinStorageLeaf("/tmp/pulse", "session.json")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ActivationStateFileName is the name of the encrypted activation state file.
|
||||
|
|
@ -36,11 +35,18 @@ func (p *Persistence) SaveActivationState(state *ActivationState) error {
|
|||
return fmt.Errorf("encrypt activation state: %w", err)
|
||||
}
|
||||
|
||||
if err := ensurePersistenceOwnerOnlyDir(p.configDir); err != nil {
|
||||
configDir, err := normalizePersistenceConfigDir(p.configDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve config directory: %w", err)
|
||||
}
|
||||
if err := ensurePersistenceOwnerOnlyDir(configDir); err != nil {
|
||||
return fmt.Errorf("secure config directory: %w", err)
|
||||
}
|
||||
|
||||
statePath := filepath.Join(p.configDir, ActivationStateFileName)
|
||||
statePath, err := resolvePersistencePath(configDir, ActivationStateFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve activation state file path: %w", err)
|
||||
}
|
||||
encoded := base64.StdEncoding.EncodeToString(encrypted)
|
||||
|
||||
if err := writeOwnerOnlyPersistenceFileAtomic(statePath, []byte(encoded)); err != nil {
|
||||
|
|
@ -53,7 +59,10 @@ func (p *Persistence) SaveActivationState(state *ActivationState) error {
|
|||
// LoadActivationState reads and decrypts the activation state from disk.
|
||||
// Returns nil, nil if no activation state file exists.
|
||||
func (p *Persistence) LoadActivationState() (*ActivationState, error) {
|
||||
statePath := filepath.Join(p.configDir, ActivationStateFileName)
|
||||
statePath, err := resolvePersistencePath(p.configDir, ActivationStateFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve activation state file path: %w", err)
|
||||
}
|
||||
|
||||
encoded, err := readBoundedPersistenceRegularFile(statePath, maxLicenseFileSize)
|
||||
if err != nil {
|
||||
|
|
@ -99,8 +108,11 @@ func (p *Persistence) LoadActivationState() (*ActivationState, error) {
|
|||
|
||||
// ClearActivationState removes the activation state file from disk.
|
||||
func (p *Persistence) ClearActivationState() error {
|
||||
statePath := filepath.Join(p.configDir, ActivationStateFileName)
|
||||
err := os.Remove(statePath)
|
||||
statePath, err := resolvePersistencePath(p.configDir, ActivationStateFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve activation state file path: %w", err)
|
||||
}
|
||||
err = os.Remove(statePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("delete activation state file: %w", err)
|
||||
}
|
||||
|
|
@ -109,7 +121,10 @@ func (p *Persistence) ClearActivationState() error {
|
|||
|
||||
// ActivationStateExists checks if an activation state file exists on disk.
|
||||
func (p *Persistence) ActivationStateExists() bool {
|
||||
statePath := filepath.Join(p.configDir, ActivationStateFileName)
|
||||
statePath, err := resolvePersistencePath(p.configDir, ActivationStateFileName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
info, err := os.Lstat(statePath)
|
||||
if err != nil {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/securityutil"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -40,6 +42,27 @@ func isMissingPersistencePathError(err error) bool {
|
|||
return errors.Is(err, os.ErrNotExist) || errors.Is(err, syscall.ENOTDIR)
|
||||
}
|
||||
|
||||
func normalizePersistenceConfigDir(configDir string) (string, error) {
|
||||
normalizedConfigDir, err := securityutil.NormalizeStorageDir(configDir)
|
||||
if err != nil {
|
||||
return "", errors.New("config directory cannot be empty")
|
||||
}
|
||||
return normalizedConfigDir, nil
|
||||
}
|
||||
|
||||
func resolvePersistencePath(configDir string, leaf string) (string, error) {
|
||||
normalizedConfigDir, err := normalizePersistenceConfigDir(configDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
path, err := securityutil.JoinStorageLeaf(normalizedConfigDir, leaf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve persistence path for %q: %w", leaf, err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func ensurePersistenceOwnerOnlyDir(dir string) error {
|
||||
if err := os.MkdirAll(dir, persistencePrivateDirPerm); err != nil {
|
||||
return err
|
||||
|
|
@ -133,13 +156,13 @@ type Persistence struct {
|
|||
// It tries to use a persistent key stored in configDir first, then falls back
|
||||
// to machine-id for backwards compatibility with existing installations.
|
||||
func NewPersistence(configDir string) (*Persistence, error) {
|
||||
configDir = strings.TrimSpace(configDir)
|
||||
if configDir == "" {
|
||||
return nil, errors.New("config directory cannot be empty")
|
||||
normalizedConfigDir, err := normalizePersistenceConfigDir(configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try to load persistent key from config directory first
|
||||
persistentKey, err := loadPersistentKey(configDir)
|
||||
persistentKey, err := loadPersistentKey(normalizedConfigDir)
|
||||
if err != nil && !isMissingPersistencePathError(err) {
|
||||
return nil, fmt.Errorf("failed to load persistent key: %w", err)
|
||||
}
|
||||
|
|
@ -157,7 +180,7 @@ func NewPersistence(configDir string) (*Persistence, error) {
|
|||
}
|
||||
|
||||
return &Persistence{
|
||||
configDir: configDir,
|
||||
configDir: normalizedConfigDir,
|
||||
encryptionKey: encryptionKey,
|
||||
machineID: machineID,
|
||||
}, nil
|
||||
|
|
@ -165,7 +188,10 @@ func NewPersistence(configDir string) (*Persistence, error) {
|
|||
|
||||
// loadPersistentKey attempts to load the persistent encryption key from disk.
|
||||
func loadPersistentKey(configDir string) (string, error) {
|
||||
keyPath := filepath.Join(configDir, PersistentKeyFileName)
|
||||
keyPath, err := resolvePersistencePath(configDir, PersistentKeyFileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data, err := readBoundedPersistenceRegularFile(keyPath, maxPersistentKeyFileSize)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -180,9 +206,16 @@ func loadPersistentKey(configDir string) (string, error) {
|
|||
// ensurePersistentKey creates a persistent encryption key if one doesn't exist.
|
||||
// Returns the key (existing or newly created).
|
||||
func (p *Persistence) ensurePersistentKey() (string, error) {
|
||||
keyPath := filepath.Join(p.configDir, PersistentKeyFileName)
|
||||
configDir, err := normalizePersistenceConfigDir(p.configDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
keyPath, err := resolvePersistencePath(configDir, PersistentKeyFileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := ensurePersistenceOwnerOnlyDir(p.configDir); err != nil {
|
||||
if err := ensurePersistenceOwnerOnlyDir(configDir); err != nil {
|
||||
return "", fmt.Errorf("failed to secure config directory: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -261,11 +294,18 @@ func (p *Persistence) SaveWithGracePeriod(licenseKey string, gracePeriodEnd *int
|
|||
return fmt.Errorf("failed to encrypt license: %w", err)
|
||||
}
|
||||
|
||||
if err := ensurePersistenceOwnerOnlyDir(p.configDir); err != nil {
|
||||
configDir, err := normalizePersistenceConfigDir(p.configDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensurePersistenceOwnerOnlyDir(configDir); err != nil {
|
||||
return fmt.Errorf("failed to secure config directory: %w", err)
|
||||
}
|
||||
|
||||
licensePath := filepath.Join(p.configDir, LicenseFileName)
|
||||
licensePath, err := resolvePersistencePath(configDir, LicenseFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve license file path: %w", err)
|
||||
}
|
||||
encoded := base64.StdEncoding.EncodeToString(encrypted)
|
||||
|
||||
if err := writeOwnerOnlyPersistenceFileAtomic(licensePath, []byte(encoded)); err != nil {
|
||||
|
|
@ -288,7 +328,10 @@ func (p *Persistence) Load() (string, error) {
|
|||
// It tries to decrypt with the current encryption key first, then falls back
|
||||
// to machine-id for backwards compatibility with existing installations.
|
||||
func (p *Persistence) LoadWithMetadata() (PersistedLicense, error) {
|
||||
licensePath := filepath.Join(p.configDir, LicenseFileName)
|
||||
licensePath, err := resolvePersistencePath(p.configDir, LicenseFileName)
|
||||
if err != nil {
|
||||
return PersistedLicense{}, fmt.Errorf("resolve license file path: %w", err)
|
||||
}
|
||||
|
||||
encoded, err := readBoundedPersistenceRegularFile(licensePath, maxLicenseFileSize)
|
||||
if err != nil {
|
||||
|
|
@ -340,8 +383,11 @@ func (p *Persistence) LoadWithMetadata() (PersistedLicense, error) {
|
|||
|
||||
// Delete removes the saved license file.
|
||||
func (p *Persistence) Delete() error {
|
||||
licensePath := filepath.Join(p.configDir, LicenseFileName)
|
||||
err := os.Remove(licensePath)
|
||||
licensePath, err := resolvePersistencePath(p.configDir, LicenseFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve license file path: %w", err)
|
||||
}
|
||||
err = os.Remove(licensePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to delete license file: %w", err)
|
||||
}
|
||||
|
|
@ -350,7 +396,10 @@ func (p *Persistence) Delete() error {
|
|||
|
||||
// Exists checks if a saved license exists.
|
||||
func (p *Persistence) Exists() bool {
|
||||
licensePath := filepath.Join(p.configDir, LicenseFileName)
|
||||
licensePath, err := resolvePersistencePath(p.configDir, LicenseFileName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
info, err := os.Lstat(licensePath)
|
||||
if err != nil {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -289,6 +289,27 @@ func TestPersistenceEnforcesOwnerOnlyPermissions(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue