1package config
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/bnema/dumber/internal/application/dto"
10 "github.com/spf13/viper"
11 "github.com/stretchr/testify/require"
12)
13
14func TestDefaultConfig_ExternalThemeDefaults(t *testing.T) {
15 cfg := DefaultConfig()
16
17 require.False(t, cfg.Appearance.ExternalTheme.Enabled)
18 require.Equal(t, "noctalia", cfg.Appearance.ExternalTheme.Provider)
19 require.Equal(t, "colors-json", cfg.Appearance.ExternalTheme.Format)
20 require.Equal(t, filepath.Join("noctalia", "colors.json"), lastPathElements(cfg.Appearance.ExternalTheme.Path, 2))
21}
22
23func TestManagerSetDefaults_ExternalThemeViperDefaults(t *testing.T) {
24 mgr := &Manager{viper: viper.New()}
25 mgr.setDefaults()
26
27 require.False(t, mgr.viper.GetBool("appearance.external_theme.enabled"))
28 require.Equal(t, "noctalia", mgr.viper.GetString("appearance.external_theme.provider"))
29 require.Equal(t, "colors-json", mgr.viper.GetString("appearance.external_theme.format"))
30 require.Empty(t, mgr.viper.GetString("appearance.external_theme.path"))
31}
32
33func TestManagerLoad_ExternalThemeEnabledEmptyPathUsesDefaultColorsJSON(t *testing.T) {
34 configHome := t.TempDir()
35 t.Setenv("ENV", "")
36 t.Setenv("XDG_CONFIG_HOME", configHome)
37
38 configDir := filepath.Join(configHome, appName)
39 require.NoError(t, os.MkdirAll(configDir, dirPerm))
40 require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.toml"), []byte(`
41[appearance.external_theme]
42enabled = true
43provider = 'noctalia'
44path = ''
45`), filePerm))
46
47 mgr, err := NewManager()
48 require.NoError(t, err)
49 require.NoError(t, mgr.Load())
50
51 externalTheme := mgr.Get().Appearance.ExternalTheme
52 require.True(t, externalTheme.Enabled)
53 require.Equal(t, "noctalia", externalTheme.Provider)
54 require.Equal(t, "colors-json", externalTheme.Format)
55 require.Equal(t, filepath.Join(configHome, "noctalia", "colors.json"), externalTheme.Path)
56}
57
58func TestManagerLoad_ExternalThemeDumberJSONEmptyPathUsesTemplateDefault(t *testing.T) {
59 configHome := t.TempDir()
60 t.Setenv("ENV", "")
61 t.Setenv("XDG_CONFIG_HOME", configHome)
62
63 configDir := filepath.Join(configHome, appName)
64 require.NoError(t, os.MkdirAll(configDir, dirPerm))
65 require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.toml"), []byte(`
66[appearance.external_theme]
67enabled = true
68provider = 'noctalia'
69format = 'dumber-json'
70path = ''
71`), filePerm))
72
73 mgr, err := NewManager()
74 require.NoError(t, err)
75 require.NoError(t, mgr.Load())
76
77 externalTheme := mgr.Get().Appearance.ExternalTheme
78 require.True(t, externalTheme.Enabled)
79 require.Equal(t, "noctalia", externalTheme.Provider)
80 require.Equal(t, "dumber-json", externalTheme.Format)
81 require.Equal(t, filepath.Join(configDir, defaultExternalThemeTemplateFilename), externalTheme.Path)
82}
83
84func TestManagerLoad_ExternalThemeDumberJSONOmittedPathUsesTemplateDefault(t *testing.T) {
85 configHome := t.TempDir()
86 t.Setenv("ENV", "")
87 t.Setenv("XDG_CONFIG_HOME", configHome)
88
89 configDir := filepath.Join(configHome, appName)
90 require.NoError(t, os.MkdirAll(configDir, dirPerm))
91 require.NoError(t, os.WriteFile(filepath.Join(configDir, "config.toml"), []byte(`
92[appearance.external_theme]
93enabled = true
94provider = 'noctalia'
95format = 'dumber-json'
96`), filePerm))
97
98 mgr, err := NewManager()
99 require.NoError(t, err)
100 require.NoError(t, mgr.Load())
101
102 externalTheme := mgr.Get().Appearance.ExternalTheme
103 require.True(t, externalTheme.Enabled)
104 require.Equal(t, "noctalia", externalTheme.Provider)
105 require.Equal(t, "dumber-json", externalTheme.Format)
106 require.Equal(t, filepath.Join(configDir, defaultExternalThemeTemplateFilename), externalTheme.Path)
107}
108
109func TestValidateConfig_ExternalTheme(t *testing.T) {
110 tests := []struct {
111 name string
112 mutate func(*Config)
113 wantErr bool
114 wantText string
115 }{
116 {
117 name: "disabled empty path validates",
118 mutate: func(cfg *Config) {
119 cfg.Appearance.ExternalTheme.Path = ""
120 },
121 },
122 {
123 name: "enabled colors-json with path validates",
124 mutate: func(cfg *Config) {
125 cfg.Appearance.ExternalTheme.Enabled = true
126 cfg.Appearance.ExternalTheme.Format = "colors-json"
127 cfg.Appearance.ExternalTheme.Path = "/tmp/colors.json"
128 },
129 },
130 {
131 name: "enabled dumber-json with path validates",
132 mutate: func(cfg *Config) {
133 cfg.Appearance.ExternalTheme.Enabled = true
134 cfg.Appearance.ExternalTheme.Format = "dumber-json"
135 cfg.Appearance.ExternalTheme.Path = "/tmp/theme.json"
136 },
137 },
138 {
139 name: "enabled empty path uses format-specific default",
140 mutate: func(cfg *Config) {
141 cfg.Appearance.ExternalTheme.Enabled = true
142 cfg.Appearance.ExternalTheme.Path = ""
143 },
144 },
145 {
146 name: "enabled rejects unsupported provider",
147 mutate: func(cfg *Config) {
148 cfg.Appearance.ExternalTheme.Enabled = true
149 cfg.Appearance.ExternalTheme.Provider = "other"
150 cfg.Appearance.ExternalTheme.Path = "/tmp/theme.json"
151 },
152 wantErr: true,
153 wantText: "appearance.external_theme.provider",
154 },
155 {
156 name: "enabled rejects unsupported format",
157 mutate: func(cfg *Config) {
158 cfg.Appearance.ExternalTheme.Enabled = true
159 cfg.Appearance.ExternalTheme.Format = "toml"
160 cfg.Appearance.ExternalTheme.Path = "/tmp/theme.json"
161 },
162 wantErr: true,
163 wantText: "appearance.external_theme.format",
164 },
165 }
166
167 for _, tt := range tests {
168 t.Run(tt.name, func(t *testing.T) {
169 cfg := DefaultConfig()
170 tt.mutate(cfg)
171 normalizeConfig(cfg)
172
173 err := validateConfig(cfg)
174 if tt.wantErr {
175 require.Error(t, err)
176 require.Contains(t, err.Error(), tt.wantText)
177 return
178 }
179 require.NoError(t, err)
180 })
181 }
182}
183
184func TestNormalizeConfig_ExternalTheme(t *testing.T) {
185 cfg := DefaultConfig()
186 cfg.Appearance.ExternalTheme.Provider = " NOCTALIA "
187 cfg.Appearance.ExternalTheme.Format = " COLORS-JSON "
188 cfg.Appearance.ExternalTheme.Path = " /tmp/colors.json "
189
190 normalizeConfig(cfg)
191
192 require.Equal(t, "noctalia", cfg.Appearance.ExternalTheme.Provider)
193 require.Equal(t, "colors-json", cfg.Appearance.ExternalTheme.Format)
194 require.Equal(t, "/tmp/colors.json", cfg.Appearance.ExternalTheme.Path)
195
196 cfg.Appearance.ExternalTheme.Provider = ""
197 cfg.Appearance.ExternalTheme.Format = ""
198 normalizeConfig(cfg)
199 require.Equal(t, "noctalia", cfg.Appearance.ExternalTheme.Provider)
200 require.Equal(t, "colors-json", cfg.Appearance.ExternalTheme.Format)
201}
202
203func TestSchemaProvider_ExternalThemeKeys(t *testing.T) {
204 keys := NewSchemaProvider().GetSchema()
205 byKey := map[string]bool{}
206 for _, key := range keys {
207 if key.Key == "appearance.external_theme.provider" {
208 require.Equal(t, SectionAppearance, key.Section)
209 require.Equal(t, "noctalia", key.Default)
210 require.Equal(t, []string{"noctalia"}, key.Values)
211 }
212 if key.Key == "appearance.external_theme.format" {
213 require.Equal(t, SectionAppearance, key.Section)
214 require.Equal(t, "colors-json", key.Default)
215 require.Equal(t, []string{"colors-json", "dumber-json"}, key.Values)
216 }
217 if key.Key == "appearance.external_theme.path" {
218 require.Equal(t, filepath.Join("noctalia", "colors.json"), lastPathElements(key.Default, 2))
219 }
220 byKey[key.Key] = true
221 }
222
223 for _, key := range []string{
224 "appearance.external_theme.enabled",
225 "appearance.external_theme.provider",
226 "appearance.external_theme.format",
227 "appearance.external_theme.path",
228 } {
229 require.True(t, byKey[key], "missing schema key %s", key)
230 }
231}
232
233func TestWriteConfigOrdered_IncludesExternalThemeTable(t *testing.T) {
234 path := filepath.Join(t.TempDir(), "config.toml")
235 cfg := DefaultConfig()
236
237 require.NoError(t, WriteConfigOrdered(cfg, path))
238 content, err := os.ReadFile(path)
239 require.NoError(t, err)
240 require.Contains(t, string(content), "[appearance.external_theme]")
241 require.Contains(t, string(content), "enabled = false")
242 require.Contains(t, string(content), "provider = 'noctalia'")
243 require.Contains(t, string(content), "format = 'colors-json'")
244 require.Contains(t, string(content), "colors.json")
245}
246
247func TestBuildSystemviewConfigPayload_ProjectsExternalTheme(t *testing.T) {
248 cfg := DefaultConfig()
249 cfg.Appearance.ExternalTheme.Enabled = true
250 cfg.Appearance.ExternalTheme.Path = "/tmp/colors.json"
251
252 payload := BuildSystemviewConfigPayload(cfg, nil)
253
254 require.True(t, payload.Appearance.ExternalTheme.Enabled)
255 require.Equal(t, "noctalia", payload.Appearance.ExternalTheme.Provider)
256 require.Equal(t, "colors-json", payload.Appearance.ExternalTheme.Format)
257 require.Equal(t, "/tmp/colors.json", payload.Appearance.ExternalTheme.Path)
258}
259
260func lastPathElements(path string, count int) string {
261 cleaned := filepath.Clean(path)
262 parts := make([]string, 0, count)
263 for len(parts) < count && cleaned != "." && cleaned != string(filepath.Separator) {
264 parts = append([]string{filepath.Base(cleaned)}, parts...)
265 cleaned = filepath.Dir(cleaned)
266 }
267 return filepath.Join(parts...)
268}
269
270func TestWebUIConfigGatewaySaveWebUIConfig_ExternalThemeRoundTrip(t *testing.T) {
271 path := filepath.Join(t.TempDir(), "config.toml")
272 require.NoError(t, os.WriteFile(path, []byte(""), 0o644))
273
274 v := viper.New()
275 v.SetConfigFile(path)
276 require.NoError(t, v.ReadInConfig())
277
278 previousGlobal := globalManager
279 defer func() { globalManager = previousGlobal }()
280
281 mgr := &Manager{config: DefaultConfig(), viper: v}
282 globalManager = mgr
283
284 webCfg := dto.WebUIConfig{
285 Appearance: buildSystemviewAppearancePayload(DefaultConfig().Appearance),
286 }
287 webCfg.Appearance.ExternalTheme = dto.WebUIExternalThemeConfig{
288 Enabled: true,
289 Provider: " NOCTALIA ",
290 Format: " COLORS-JSON ",
291 Path: " /tmp/colors.json ",
292 }
293 webCfg.DefaultUIScale = DefaultConfig().DefaultUIScale
294 webCfg.DefaultSearchEngine = DefaultConfig().DefaultSearchEngine
295 webCfg.SearchShortcuts = map[string]dto.SearchShortcut{}
296 for key, shortcut := range DefaultConfig().SearchShortcuts {
297 webCfg.SearchShortcuts[key] = dto.SearchShortcut{URL: shortcut.URL, Description: shortcut.Description}
298 }
299
300 require.NoError(t, NewWebUIConfigGateway(mgr).SaveWebUIConfig(context.Background(), webCfg))
301
302 saved := mgr.Get()
303 require.True(t, saved.Appearance.ExternalTheme.Enabled)
304 require.Equal(t, "noctalia", saved.Appearance.ExternalTheme.Provider)
305 require.Equal(t, "colors-json", saved.Appearance.ExternalTheme.Format)
306 require.Equal(t, "/tmp/colors.json", saved.Appearance.ExternalTheme.Path)
307}