From bf30a094bb56f9c1e54eec7df5a115ddfdfa4831 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 12 Apr 2026 11:19:34 +0100 Subject: [PATCH] Own ConfigWatcher API token persistence --- internal/config/watcher.go | 15 ++++++++++++--- internal/config/watcher_extra_test.go | 6 +----- internal/config/watcher_test.go | 22 +++++----------------- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/internal/config/watcher.go b/internal/config/watcher.go index 71e86af10..01aa22ad5 100644 --- a/internal/config/watcher.go +++ b/internal/config/watcher.go @@ -31,6 +31,7 @@ type ConfigWatcher struct { config *Config envPath string apiTokensPath string + persistence *ConfigPersistence watcher *fsnotify.Watcher stopChan chan struct{} stopOnce sync.Once // Ensures Stop() can only close channel once @@ -81,11 +82,19 @@ func NewConfigWatcher(config *Config) (*ConfigWatcher, error) { } apiTokensPath := filepath.Join(filepath.Dir(envPath), "api_tokens.json") + persistence, persistenceErr := newConfigPersistence(persistentDataDir) + if persistenceErr != nil { + log.Warn(). + Err(persistenceErr). + Str("configDir", persistentDataDir). + Msg("Config watcher persistence unavailable; API token reloads will be disabled") + } cw := &ConfigWatcher{ config: config, envPath: envPath, apiTokensPath: apiTokensPath, + persistence: persistence, watcher: watcher, stopChan: make(chan struct{}), pollInterval: 5 * time.Second, @@ -412,7 +421,7 @@ func (cw *ConfigWatcher) reloadAPITokens() { cw.mu.Lock() defer cw.mu.Unlock() - if globalPersistence == nil { + if cw.persistence == nil { log.Warn().Str("api_tokens_path", cw.apiTokensPath).Msg("Config persistence unavailable; cannot reload API tokens") return } @@ -428,7 +437,7 @@ func (cw *ConfigWatcher) reloadAPITokens() { retryDelay := 50 * time.Millisecond for attempt := 1; attempt <= maxRetries; attempt++ { - tokens, err = globalPersistence.LoadAPITokens() + tokens, err = cw.persistence.LoadAPITokens() if err == nil { break } @@ -476,7 +485,7 @@ func (cw *ConfigWatcher) reloadAPITokens() { } if persistMutations { - if err := globalPersistence.SaveAPITokens(tokens); err != nil { + if err := cw.persistence.SaveAPITokens(tokens); err != nil { log.Error(). Err(err). Str("api_tokens_path", cw.apiTokensPath). diff --git a/internal/config/watcher_extra_test.go b/internal/config/watcher_extra_test.go index 89ee39844..59710d9ff 100644 --- a/internal/config/watcher_extra_test.go +++ b/internal/config/watcher_extra_test.go @@ -28,6 +28,7 @@ func TestConfigWatcher_WatchForChanges_Live(t *testing.T) { require.NoError(t, os.WriteFile(apiTokensPath, []byte("[]"), 0o644)) t.Setenv("PULSE_AUTH_CONFIG_DIR", tempDir) + t.Setenv("PULSE_MOCK_TEST", "before") cfg := &Config{} cw, err := NewConfigWatcher(cfg) @@ -38,15 +39,10 @@ func TestConfigWatcher_WatchForChanges_Live(t *testing.T) { cw.SetMockReloadCallback(func() { mockReloaded <- true }) cw.SetAPITokenReloadCallback(func() { tokensReloaded <- true }) - p := NewConfigPersistence(tempDir) - originalPersistence := globalPersistence - globalPersistence = p - err = cw.Start() require.NoError(t, err) t.Cleanup(func() { cw.Stop() - globalPersistence = originalPersistence }) require.NoError(t, os.WriteFile(envPath, []byte("PULSE_AUTH_USER=something-different\nPULSE_MOCK_TEST=2"), 0o644)) diff --git a/internal/config/watcher_test.go b/internal/config/watcher_test.go index 5f455c87e..c18dbd9bd 100644 --- a/internal/config/watcher_test.go +++ b/internal/config/watcher_test.go @@ -145,17 +145,13 @@ func TestConfigWatcher_ReloadAPITokens(t *testing.T) { tempDir := t.TempDir() p := NewConfigPersistence(tempDir) - // Save globalPersistence to restore later - originalPersistence := globalPersistence - globalPersistence = p - defer func() { globalPersistence = originalPersistence }() - // Setup Watcher apiTokensPath := filepath.Join(tempDir, "api_tokens.json") cfg := &Config{} cw := &ConfigWatcher{ config: cfg, apiTokensPath: apiTokensPath, + persistence: p, } var callbackCalled atomic.Bool @@ -261,11 +257,7 @@ func TestConfigWatcher_PollForChanges(t *testing.T) { cw.SetMockReloadCallback(func() { mockCalled.Store(true) }) cw.SetAPITokenReloadCallback(func() { tokenCalled.Store(true) }) - // Mock global persistence for API token reloads - p := NewConfigPersistence(tempDir) - originalPersistence := globalPersistence - globalPersistence = p - defer func() { globalPersistence = originalPersistence }() + require.NotNil(t, cw.persistence) // Run pollForChanges in background go cw.pollForChanges() @@ -293,10 +285,9 @@ func TestConfigWatcher_PollForChanges(t *testing.T) { // 3. Update api_tokens.json // Write valid JSON - // We need to write to file that Persistence reads. - // ReloadAPITokens uses globalPersistence to load. + // Write to the watcher-owned persistence so reload reads the same installation scope. tokens := []APITokenRecord{{ID: "new", Hash: "hash", Name: "New"}} - require.NoError(t, p.SaveAPITokens(tokens)) + require.NoError(t, cw.persistence.SaveAPITokens(tokens)) future = future.Add(2 * time.Second) require.NoError(t, os.Chtimes(apiTokensPath, future, future)) @@ -418,10 +409,6 @@ func TestConfigWatcher_ReloadAPITokens_Retries(t *testing.T) { tempDir := t.TempDir() p := NewConfigPersistence(tempDir) - originalPersistence := globalPersistence - globalPersistence = p - defer func() { globalPersistence = originalPersistence }() - apiTokensPath := filepath.Join(tempDir, "api_tokens.json") require.NoError(t, os.WriteFile(apiTokensPath, []byte("{invalid-json"), 0644)) @@ -429,6 +416,7 @@ func TestConfigWatcher_ReloadAPITokens_Retries(t *testing.T) { cw := &ConfigWatcher{ config: cfg, apiTokensPath: apiTokensPath, + persistence: p, } // Should attempt retries and log errors but continue