mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
fix(service): detect first run by settings.json absence, not empty server_url
Startup treated an empty server_url as "first run" and wrote a fresh default settings.json via createDefaultSettings, which builds the struct from CLI flags and does not merge the existing file. A hand-authored settings.json that sets, say, trust_forwarded_headers but leaves server_url to the --server-url flag has no server_url, so it was silently clobbered on first start (losing the operator's keys). Gate the default-seed (and the lost-volume "first run" notice) on the ABSENCE of settings.json instead. An existing file is now always respected; a genuinely empty data dir still gets defaults and the notice. This also fixes a latent loop where a never-set server_url made every start look like a first run. Adds regression tests: settingsFileExists, plus first-run seed both preserving a hand-authored file and writing defaults when absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d31710bd8e
commit
1cac9989be
@@ -481,9 +481,18 @@ func main() {
|
||||
config := loadConfig(c)
|
||||
ds := initDataStore(config.dataDir)
|
||||
|
||||
// Detect a genuinely fresh data dir by the ABSENCE of settings.json,
|
||||
// not by an empty server_url. A hand-authored settings.json (e.g. one
|
||||
// that only sets trust_forwarded_headers and leaves server_url to the
|
||||
// --server-url flag) exists but has no server_url; keying the "first
|
||||
// run" default-write off server_url would treat it as fresh and
|
||||
// clobber the operator's file, dropping fields createDefaultSettings
|
||||
// doesn't know about.
|
||||
settingsExisted := settingsFileExists(config.dataDir)
|
||||
|
||||
persisted := applyPersistedSettings(ds, &config)
|
||||
|
||||
if persisted.ServerURL == "" {
|
||||
if !settingsExisted {
|
||||
log.Printf("Creating default settings.json in %s", sanitizeLog(config.dataDir))
|
||||
log.Printf("Data directory %s looks empty (first run). If you did NOT expect this "+
|
||||
"(e.g. after recreating a Docker container), your previous settings, datastore and "+
|
||||
@@ -913,6 +922,20 @@ func getDomains(serverURL, httpsServerURL, hostname string, extraHosts []string)
|
||||
return domains
|
||||
}
|
||||
|
||||
// settingsFileExists reports whether a settings.json is already present in the
|
||||
// data dir. It's the first-run discriminator: an existing file (even an
|
||||
// incomplete, hand-authored one) must never be overwritten by the default
|
||||
// seed, while a truly empty data dir gets defaults plus the lost-volume notice.
|
||||
func settingsFileExists(dataDir string) bool {
|
||||
if dataDir == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := os.Stat(filepath.Join(dataDir, "settings.json"))
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func applyPersistedSettings(ds *datastore.DataStore, config *serviceConfig) datastore.Settings {
|
||||
persisted, err := ds.GetSettings()
|
||||
if err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
@@ -187,3 +188,83 @@ func contains(haystack []string, needle string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func TestSettingsFileExists(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
if settingsFileExists(dir) {
|
||||
t.Fatal("expected false for a dir without settings.json")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(dir, "settings.json"), []byte("{}"), 0o644); err != nil {
|
||||
t.Fatalf("write settings.json: %v", err)
|
||||
}
|
||||
|
||||
if !settingsFileExists(dir) {
|
||||
t.Fatal("expected true once settings.json is present")
|
||||
}
|
||||
|
||||
if settingsFileExists("") {
|
||||
t.Fatal("expected false for an empty data dir")
|
||||
}
|
||||
}
|
||||
|
||||
// applyFirstRunSeed mirrors the startup gate in the CLI Action: a default
|
||||
// settings.json is written only when none exists yet, so a hand-authored file
|
||||
// is never clobbered.
|
||||
func applyFirstRunSeed(ds *datastore.DataStore, config *serviceConfig) {
|
||||
existed := settingsFileExists(config.dataDir)
|
||||
|
||||
applyPersistedSettings(ds, config)
|
||||
|
||||
if !existed {
|
||||
createDefaultSettings(ds, *config)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstRunSeed_PreservesHandAuthoredSettings(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Operator pre-seeds proxy trust but leaves server_url to the --server-url
|
||||
// flag. Before the fix this was treated as "first run" and overwritten.
|
||||
if err := os.WriteFile(filepath.Join(dir, "settings.json"),
|
||||
[]byte(`{"trust_forwarded_headers":true,"trusted_proxy_cidrs":["10.0.0.0/8"]}`), 0o644); err != nil {
|
||||
t.Fatalf("write settings.json: %v", err)
|
||||
}
|
||||
|
||||
ds := datastore.NewDataStore(dir)
|
||||
config := &serviceConfig{dataDir: dir, serverURL: "http://192.0.2.1:8000"}
|
||||
|
||||
applyFirstRunSeed(ds, config)
|
||||
|
||||
got, err := ds.GetSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("GetSettings: %v", err)
|
||||
}
|
||||
|
||||
if !got.TrustForwardedHeaders {
|
||||
t.Error("trust_forwarded_headers was clobbered on startup")
|
||||
}
|
||||
|
||||
if len(got.TrustedProxyCIDRs) != 1 || got.TrustedProxyCIDRs[0] != "10.0.0.0/8" {
|
||||
t.Errorf("trusted_proxy_cidrs was clobbered, got %v", got.TrustedProxyCIDRs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstRunSeed_WritesDefaultsWhenAbsent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
ds := datastore.NewDataStore(dir)
|
||||
config := &serviceConfig{dataDir: dir, serverURL: "http://192.0.2.1:8000"}
|
||||
|
||||
applyFirstRunSeed(ds, config)
|
||||
|
||||
got, err := ds.GetSettings()
|
||||
if err != nil {
|
||||
t.Fatalf("GetSettings: %v", err)
|
||||
}
|
||||
|
||||
if got.ServerURL != "http://192.0.2.1:8000" {
|
||||
t.Errorf("expected defaults to be written with server_url, got %q", got.ServerURL)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user