diff --git a/internal/api/auth.go b/internal/api/auth.go index 81acc5e5f..b75a24487 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -206,6 +206,11 @@ func InitSessionStore(dataPath string) { } } +func InitPersistentAuthStores(dataPath string) { + InitSessionStore(dataPath) + InitCSRFStore(dataPath) +} + // GetSessionStore returns the global session store instance func GetSessionStore() *SessionStore { sessionStoreMu.Lock() diff --git a/internal/api/cloud_handoff.go b/internal/api/cloud_handoff.go index d5b95d799..f2144d50b 100644 --- a/internal/api/cloud_handoff.go +++ b/internal/api/cloud_handoff.go @@ -20,6 +20,7 @@ import ( // Self-guards: returns 404 if the handoff key file does not exist in dataPath, // meaning this is not a cloud-managed tenant. func HandleCloudHandoff(dataPath string) http.HandlerFunc { + InitPersistentAuthStores(dataPath) replay := &jtiReplayStore{configDir: dataPath} return func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/cloud_handoff_handlers.go b/internal/api/cloud_handoff_handlers.go index 559a06d9e..d9e99bd5f 100644 --- a/internal/api/cloud_handoff_handlers.go +++ b/internal/api/cloud_handoff_handlers.go @@ -281,6 +281,7 @@ func normalizeHandoffEmail(email string) string { // this returns a success payload instead of redirecting. func HandleHandoffExchange(configDir string) http.HandlerFunc { configDir = filepath.Clean(configDir) + InitPersistentAuthStores(configDir) keyPath := filepath.Join(configDir, "secrets", "handoff.key") replay := &jtiReplayStore{configDir: configDir} diff --git a/internal/api/cloud_handoff_test.go b/internal/api/cloud_handoff_test.go index 9fbe478a7..341c1e04d 100644 --- a/internal/api/cloud_handoff_test.go +++ b/internal/api/cloud_handoff_test.go @@ -14,6 +14,8 @@ import ( ) func TestHandleCloudHandoffRejectsReplay(t *testing.T) { + resetPersistentAuthStoresForTests() + t.Cleanup(resetPersistentAuthStoresForTests) dataPath := t.TempDir() key := []byte("0123456789abcdef0123456789abcdef") if err := os.WriteFile(filepath.Join(dataPath, cloudauth.HandoffKeyFile), key, 0o600); err != nil { @@ -54,6 +56,8 @@ func TestHandleCloudHandoffRejectsReplay(t *testing.T) { } func TestHandleCloudHandoffSetsTenantOrgCookie(t *testing.T) { + resetPersistentAuthStoresForTests() + t.Cleanup(resetPersistentAuthStoresForTests) dataPath := t.TempDir() key := []byte("0123456789abcdef0123456789abcdef") if err := os.WriteFile(filepath.Join(dataPath, cloudauth.HandoffKeyFile), key, 0o600); err != nil { @@ -91,6 +95,8 @@ func TestHandleCloudHandoffSetsTenantOrgCookie(t *testing.T) { } func TestHandleCloudHandoffRejectsInvalidTenantID(t *testing.T) { + resetPersistentAuthStoresForTests() + t.Cleanup(resetPersistentAuthStoresForTests) dataPath := t.TempDir() key := []byte("0123456789abcdef0123456789abcdef") if err := os.WriteFile(filepath.Join(dataPath, cloudauth.HandoffKeyFile), key, 0o600); err != nil { @@ -117,6 +123,8 @@ func TestHandleCloudHandoffRejectsInvalidTenantID(t *testing.T) { } func TestHandleCloudHandoffLowercasesSessionEmailIdentity(t *testing.T) { + resetPersistentAuthStoresForTests() + t.Cleanup(resetPersistentAuthStoresForTests) dataPath := t.TempDir() key := []byte("0123456789abcdef0123456789abcdef") if err := os.WriteFile(filepath.Join(dataPath, cloudauth.HandoffKeyFile), key, 0o600); err != nil { diff --git a/internal/api/csrf_store.go b/internal/api/csrf_store.go index 6167519cf..142673c7b 100644 --- a/internal/api/csrf_store.go +++ b/internal/api/csrf_store.go @@ -30,6 +30,7 @@ type CSRFTokenStore struct { dataPath string saveTicker *time.Ticker stopChan chan bool + workerDone chan struct{} stopOnce sync.Once } @@ -112,9 +113,10 @@ func InitCSRFStore(dataPath string) { oldStore := csrfStore csrfStore = &CSRFTokenStore{ - tokens: make(map[string]*CSRFToken), - dataPath: newDataPath, - stopChan: make(chan bool), + tokens: make(map[string]*CSRFToken), + dataPath: newDataPath, + stopChan: make(chan bool), + workerDone: make(chan struct{}), } csrfStoreDataPath = newDataPath @@ -154,6 +156,9 @@ func (c *CSRFTokenStore) Shutdown() { default: } }) + if c.workerDone != nil { + <-c.workerDone + } } func resetCSRFStoreForTests() { @@ -169,6 +174,7 @@ func resetCSRFStoreForTests() { // backgroundWorker handles periodic saves and cleanup func (c *CSRFTokenStore) backgroundWorker() { + defer close(c.workerDone) for { select { case <-c.saveTicker.C: diff --git a/internal/api/session_store.go b/internal/api/session_store.go index 59bd365a2..60ace815c 100644 --- a/internal/api/session_store.go +++ b/internal/api/session_store.go @@ -20,6 +20,7 @@ type SessionStore struct { dataPath string saveTicker *time.Ticker stopChan chan bool + workerDone chan struct{} stopOnce sync.Once crypto *crypto.CryptoManager } @@ -150,10 +151,11 @@ func NewSessionStore(dataPath string) *SessionStore { } store := &SessionStore{ - sessions: make(map[string]*SessionData), - dataPath: dataPath, - stopChan: make(chan bool), - crypto: cm, + sessions: make(map[string]*SessionData), + dataPath: dataPath, + stopChan: make(chan bool), + workerDone: make(chan struct{}), + crypto: cm, } // Load existing sessions from disk @@ -179,10 +181,14 @@ func (s *SessionStore) Shutdown() { default: } }) + if s.workerDone != nil { + <-s.workerDone + } } // backgroundWorker handles periodic saves and cleanup func (s *SessionStore) backgroundWorker() { + defer close(s.workerDone) for { select { case <-s.saveTicker.C: diff --git a/scripts/installtests/install_sh_test.go b/scripts/installtests/install_sh_test.go index 1c2ff253f..8626d2428 100644 --- a/scripts/installtests/install_sh_test.go +++ b/scripts/installtests/install_sh_test.go @@ -776,6 +776,32 @@ func extractRootInstallShellFunction(t *testing.T, name string) string { return string(match) } +func extractSetupAutoUpdatesShellFunctions(t *testing.T) string { + t.Helper() + + return extractRootInstallShellFunction(t, "selected_update_channel") + "\n" + + extractRootInstallShellFunction(t, "repo_web_url") + "\n" + + extractRootInstallShellFunction(t, "configure_auto_update_script_repo") + "\n" + + extractRootInstallShellFunction(t, "setup_auto_updates") +} + +func prepareAutoUpdatePaths(t *testing.T, tmpDir string) (string, string, string) { + t.Helper() + + autoUpdateDest := filepath.Join(tmpDir, "bin", "pulse-auto-update.sh") + servicePath := filepath.Join(tmpDir, "systemd", "pulse-update.service") + timerPath := filepath.Join(tmpDir, "systemd", "pulse-update.timer") + + if err := os.MkdirAll(filepath.Dir(autoUpdateDest), 0755); err != nil { + t.Fatalf("mkdir auto-update dest dir: %v", err) + } + if err := os.MkdirAll(filepath.Dir(servicePath), 0755); err != nil { + t.Fatalf("mkdir systemd dir: %v", err) + } + + return autoUpdateDest, servicePath, timerPath +} + func extractAutoUpdateFunction(t *testing.T, name string) string { t.Helper() @@ -1620,16 +1646,13 @@ func TestSelectedUpdateChannelTreatsPrereleaseVersionAsRC(t *testing.T) { func TestSetupAutoUpdatesCreatesSystemJSONWithSelectedChannel(t *testing.T) { tmpDir := t.TempDir() configDir := filepath.Join(tmpDir, "config") - systemdDir := filepath.Join(tmpDir, "systemd") installDir := filepath.Join(tmpDir, "install") autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh") + autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir) if err := os.MkdirAll(configDir, 0755); err != nil { t.Fatalf("mkdir config dir: %v", err) } - if err := os.MkdirAll(systemdDir, 0755); err != nil { - t.Fatalf("mkdir systemd dir: %v", err) - } if err := os.MkdirAll(filepath.Dir(autoUpdateSrc), 0755); err != nil { t.Fatalf("mkdir auto-update src dir: %v", err) } @@ -1640,6 +1663,9 @@ func TestSetupAutoUpdatesCreatesSystemJSONWithSelectedChannel(t *testing.T) { script := ` CONFIG_DIR="` + configDir + `" INSTALL_DIR="` + installDir + `" + PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `" + PULSE_UPDATE_SERVICE_PATH="` + servicePath + `" + PULSE_UPDATE_TIMER_PATH="` + timerPath + `" FORCE_CHANNEL="rc" UPDATE_CHANNEL="" GITHUB_REPO="rcourtman/Pulse" @@ -1653,8 +1679,7 @@ func TestSetupAutoUpdatesCreatesSystemJSONWithSelectedChannel(t *testing.T) { chown() { :; } cat() { command cat "$@"; } mkdir() { command mkdir "$@"; } -` + extractRootInstallShellFunction(t, "selected_update_channel") + ` -` + extractRootInstallShellFunction(t, "setup_auto_updates") + ` +` + extractSetupAutoUpdatesShellFunctions(t) + ` setup_auto_updates ` @@ -1728,10 +1753,7 @@ func TestSetupAutoUpdatesConfiguresInstalledAutoUpdateRepo(t *testing.T) { rm() { command rm "$@"; } awk() { command awk "$@"; } mktemp() { command mktemp "$@"; } -` + extractRootInstallShellFunction(t, "selected_update_channel") + ` -` + extractRootInstallShellFunction(t, "repo_web_url") + ` -` + extractRootInstallShellFunction(t, "configure_auto_update_script_repo") + ` -` + extractRootInstallShellFunction(t, "setup_auto_updates") + ` +` + extractSetupAutoUpdatesShellFunctions(t) + ` setup_auto_updates ` @@ -1779,6 +1801,7 @@ func TestSetupAutoUpdatesTreatsPrereleaseVersionAsRCChannel(t *testing.T) { configDir := filepath.Join(tmpDir, "config") installDir := filepath.Join(tmpDir, "install") autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh") + autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir) if err := os.MkdirAll(configDir, 0755); err != nil { t.Fatalf("mkdir config dir: %v", err) @@ -1793,6 +1816,9 @@ func TestSetupAutoUpdatesTreatsPrereleaseVersionAsRCChannel(t *testing.T) { script := ` CONFIG_DIR="` + configDir + `" INSTALL_DIR="` + installDir + `" + PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `" + PULSE_UPDATE_SERVICE_PATH="` + servicePath + `" + PULSE_UPDATE_TIMER_PATH="` + timerPath + `" FORCE_CHANNEL="" FORCE_VERSION="v1.2.3-rc.4" UPDATE_CHANNEL="" @@ -1803,8 +1829,7 @@ func TestSetupAutoUpdatesTreatsPrereleaseVersionAsRCChannel(t *testing.T) { safe_systemctl() { :; } systemctl() { return 0; } chown() { :; } -` + extractRootInstallShellFunction(t, "selected_update_channel") + ` -` + extractRootInstallShellFunction(t, "setup_auto_updates") + ` +` + extractSetupAutoUpdatesShellFunctions(t) + ` setup_auto_updates ` @@ -1831,6 +1856,7 @@ func TestSetupAutoUpdatesPreservesRCChannelWhenUpdatingExistingConfig(t *testing configDir := filepath.Join(tmpDir, "config") installDir := filepath.Join(tmpDir, "install") autoUpdateSrc := filepath.Join(installDir, "scripts", "pulse-auto-update.sh") + autoUpdateDest, servicePath, timerPath := prepareAutoUpdatePaths(t, tmpDir) if err := os.MkdirAll(configDir, 0755); err != nil { t.Fatalf("mkdir config dir: %v", err) @@ -1848,6 +1874,9 @@ func TestSetupAutoUpdatesPreservesRCChannelWhenUpdatingExistingConfig(t *testing script := ` CONFIG_DIR="` + configDir + `" INSTALL_DIR="` + installDir + `" + PULSE_AUTO_UPDATE_DEST="` + autoUpdateDest + `" + PULSE_UPDATE_SERVICE_PATH="` + servicePath + `" + PULSE_UPDATE_TIMER_PATH="` + timerPath + `" FORCE_CHANNEL="" UPDATE_CHANNEL="" GITHUB_REPO="rcourtman/Pulse" @@ -1858,8 +1887,7 @@ func TestSetupAutoUpdatesPreservesRCChannelWhenUpdatingExistingConfig(t *testing systemctl() { return 0; } command -v jq >/dev/null 2>&1 || true chown() { :; } -` + extractRootInstallShellFunction(t, "selected_update_channel") + ` -` + extractRootInstallShellFunction(t, "setup_auto_updates") + ` +` + extractSetupAutoUpdatesShellFunctions(t) + ` setup_auto_updates `