config_test.go (6082B)
1 package config 2 3 import ( 4 "encoding/json" 5 "os" 6 "path/filepath" 7 "runtime" 8 "strings" 9 "testing" 10 ) 11 12 func TestConfigLifecycleAndEnvironmentPrecedence(t *testing.T) { 13 home := t.TempDir() 14 env := map[string]string{"HOME": home} 15 if runtime.GOOS == "windows" { 16 env["APPDATA"] = filepath.Join(home, "roaming") 17 } else if runtime.GOOS != "darwin" { 18 env["XDG_CONFIG_HOME"] = filepath.Join(home, "xdg") 19 } 20 21 stored := Config{ 22 BaseURL: "https://stored.example", 23 Theme: ThemeDark, 24 Token: "stored-secret", 25 LastTab: FeedToMe, 26 } 27 if err := Save(stored, env); err != nil { 28 t.Fatalf("Save: %v", err) 29 } 30 31 path := ConfigPath(env) 32 info, err := os.Stat(path) 33 if err != nil { 34 t.Fatalf("Stat(%q): %v", path, err) 35 } 36 if runtime.GOOS != "windows" && info.Mode().Perm() != 0o600 { 37 t.Fatalf("config mode = %04o, want 0600", info.Mode().Perm()) 38 } 39 40 loaded, err := Load(map[string]string{ 41 "HOME": home, 42 "APPDATA": env["APPDATA"], 43 "XDG_CONFIG_HOME": env["XDG_CONFIG_HOME"], 44 "TEXTLOG_URL": "https://override.example/", 45 "TEXTLOG_TOKEN": "environment-secret", 46 "TEXTLOG_THEME": "light", 47 }) 48 if err != nil { 49 t.Fatalf("Load: %v", err) 50 } 51 want := Config{ 52 BaseURL: "https://override.example", 53 Theme: ThemeLight, 54 Token: "environment-secret", 55 LastTab: FeedToMe, 56 } 57 if loaded != want { 58 t.Fatalf("Load = %#v, want %#v", loaded, want) 59 } 60 61 data, err := os.ReadFile(path) 62 if err != nil { 63 t.Fatal(err) 64 } 65 var unchanged Config 66 if err := json.Unmarshal(data, &unchanged); err != nil { 67 t.Fatalf("saved JSON: %v", err) 68 } 69 if unchanged != stored { 70 t.Fatalf("environment changed stored config to %#v", unchanged) 71 } 72 if !strings.HasSuffix(string(data), "\n") { 73 t.Fatalf("saved JSON has no trailing newline: %q", data) 74 } 75 } 76 77 func TestLoadDefaultsAndSanitizesStoredSelections(t *testing.T) { 78 home := t.TempDir() 79 env := isolatedEnv(home) 80 path := ConfigPath(env) 81 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { 82 t.Fatal(err) 83 } 84 if err := os.WriteFile(path, []byte(`{"baseUrl":"https://stored.example/","theme":"sepia","token":"saved","lastTab":"live"}`), 0o600); err != nil { 85 t.Fatal(err) 86 } 87 88 got, err := Load(env) 89 if err != nil { 90 t.Fatal(err) 91 } 92 if got.BaseURL != "https://stored.example" || got.Theme != ThemeAuto || got.Token != "saved" || got.LastTab != "" { 93 t.Fatalf("Load invalid selections = %#v", got) 94 } 95 96 env["NO_COLOR"] = "" 97 env["TEXTLOG_THEME"] = "dark" 98 got, err = Load(env) 99 if err != nil { 100 t.Fatal(err) 101 } 102 if got.Theme != ThemeLight { 103 t.Fatalf("NO_COLOR theme = %q, want %q", got.Theme, ThemeLight) 104 } 105 if !got.NoColor { 106 t.Fatal("NO_COLOR was not retained for rendering") 107 } 108 109 missing, err := Load(isolatedEnv(t.TempDir())) 110 if err != nil { 111 t.Fatal(err) 112 } 113 if missing != (Config{BaseURL: DefaultURL, Theme: ThemeAuto}) { 114 t.Fatalf("missing config = %#v", missing) 115 } 116 117 malformedEnv := isolatedEnv(t.TempDir()) 118 malformedPath := ConfigPath(malformedEnv) 119 if err := os.MkdirAll(filepath.Dir(malformedPath), 0o700); err != nil { 120 t.Fatal(err) 121 } 122 if err := os.WriteFile(malformedPath, []byte(`{"baseUrl":"https://partial.example",`), 0o600); err != nil { 123 t.Fatal(err) 124 } 125 malformed, err := Load(malformedEnv) 126 if err != nil { 127 t.Fatal(err) 128 } 129 if malformed != (Config{BaseURL: DefaultURL, Theme: ThemeAuto}) { 130 t.Fatalf("partially decoded malformed config = %#v", malformed) 131 } 132 } 133 134 func TestSavePersistsTheProvidedConfigWithoutApplyingEnvironment(t *testing.T) { 135 env := isolatedEnv(t.TempDir()) 136 env["TEXTLOG_URL"] = "https://override.example" 137 env["TEXTLOG_THEME"] = "light" 138 want := Config{BaseURL: "https://stored.example/", Theme: ThemeDark, Token: "stored"} 139 if err := Save(want, env); err != nil { 140 t.Fatal(err) 141 } 142 data, err := os.ReadFile(ConfigPath(env)) 143 if err != nil { 144 t.Fatal(err) 145 } 146 var got Config 147 if err := json.Unmarshal(data, &got); err != nil { 148 t.Fatal(err) 149 } 150 if got != want { 151 t.Fatalf("saved config = %#v, want %#v", got, want) 152 } 153 } 154 155 func TestNormalizeBaseURL(t *testing.T) { 156 valid := map[string]string{ 157 "https://textlog.cc/": "https://textlog.cc", 158 "http://localhost:3000/api///": "http://localhost:3000/api", 159 " https://EXAMPLE.test/a/ ": "https://example.test/a", 160 } 161 for input, want := range valid { 162 got, err := NormalizeBaseURL(input) 163 if err != nil || got != want { 164 t.Errorf("NormalizeBaseURL(%q) = %q, %v; want %q", input, got, err, want) 165 } 166 } 167 168 for _, input := range []string{"wat", "file:///tmp/no", "https://example.test?q=one", "https://example.test/#frag", "https:///missing-host"} { 169 if _, err := NormalizeBaseURL(input); err == nil { 170 t.Errorf("NormalizeBaseURL(%q) unexpectedly succeeded", input) 171 } 172 } 173 } 174 175 func TestPlatformConfigPaths(t *testing.T) { 176 env := map[string]string{ 177 "APPDATA": filepath.FromSlash("/roaming"), 178 "XDG_CONFIG_HOME": filepath.FromSlash("/xdg"), 179 } 180 tests := map[string]string{ 181 "windows": filepath.Join(env["APPDATA"], "textlog", "config.json"), 182 "darwin": filepath.Join("/home/alice", "Library", "Application Support", "textlog", "config.json"), 183 "linux": filepath.Join(env["XDG_CONFIG_HOME"], "textlog", "config.json"), 184 } 185 for goos, want := range tests { 186 if got := configPathFor(goos, "/home/alice", env); got != want { 187 t.Errorf("configPathFor(%q) = %q, want %q", goos, got, want) 188 } 189 } 190 } 191 192 func isolatedEnv(home string) map[string]string { 193 env := map[string]string{"HOME": home} 194 if runtime.GOOS == "windows" { 195 env["APPDATA"] = filepath.Join(home, "roaming") 196 } else if runtime.GOOS != "darwin" { 197 env["XDG_CONFIG_HOME"] = filepath.Join(home, "xdg") 198 } 199 return env 200 } 201 func TestLoadDoesNotTrustColorFGBG(t *testing.T) { 202 home := t.TempDir() 203 light, err := Load(map[string]string{"HOME": home, "XDG_CONFIG_HOME": home, "COLORFGBG": "0;15"}) 204 if err != nil { 205 t.Fatal(err) 206 } 207 dark, err := Load(map[string]string{"HOME": home, "XDG_CONFIG_HOME": home, "COLORFGBG": "15;0"}) 208 if err != nil { 209 t.Fatal(err) 210 } 211 if light != dark || light.Theme != ThemeAuto { 212 t.Fatalf("COLORFGBG changed automatic configuration: light=%#v dark=%#v", light, dark) 213 } 214 }