1package config
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10)
11
12func TestMigrator_NewMigrator(t *testing.T) {
13 m := NewMigrator()
14 require.NotNil(t, m)
15 require.NotNil(t, m.defaultViper)
16 require.NotNil(t, m.defaultConfig)
17}
18
19func TestMigrator_GetAllDefaultKeys(t *testing.T) {
20 m := NewMigrator()
21 keys := m.getAllDefaultKeys()
22
23 // Should have many default keys
24 assert.Greater(t, len(keys), 50, "should have many default keys")
25
26 // Should not include database.path (it's dynamically set)
27 for _, key := range keys {
28 assert.NotEqual(t, "database.path", key, "should not include database.path")
29 }
30
31 // Should include some known keys
32 keySet := make(map[string]bool)
33 for _, k := range keys {
34 keySet[k] = true
35 }
36
37 assert.True(t, keySet["history.max_entries"], "should include history.max_entries")
38 assert.True(t, keySet["logging.level"], "should include logging.level")
39 assert.True(t, keySet["appearance.sans_font"], "should include appearance.sans_font")
40}
41
42func TestMigrator_GetUserConfigKeys(t *testing.T) {
43 m := NewMigrator()
44
45 // Create a temp config file
46 tmpDir := t.TempDir()
47 configFile := filepath.Join(tmpDir, "config.toml")
48
49 content := `
50[history]
51max_entries = 5000
52retention_period_days = 180
53
54[logging]
55level = "debug"
56
57[appearance]
58sans_font = "Arial"
59`
60 err := os.WriteFile(configFile, []byte(content), 0o644)
61 require.NoError(t, err)
62
63 keys, err := m.getUserConfigKeys(configFile)
64 require.NoError(t, err)
65
66 // Should have the keys we defined
67 assert.True(t, keys["history.max_entries"])
68 assert.True(t, keys["history.retention_period_days"])
69 assert.True(t, keys["logging.level"])
70 assert.True(t, keys["appearance.sans_font"])
71
72 // Should not have keys we didn't define
73 assert.False(t, keys["history.cleanup_interval_days"])
74 assert.False(t, keys["logging.format"])
75}
76
77func TestMigrator_FlattenMap(t *testing.T) {
78 m := NewMigrator()
79
80 tests := []struct {
81 name string
82 input map[string]any
83 prefix string
84 expected []string
85 }{
86 {
87 name: "simple flat map",
88 input: map[string]any{
89 "key1": "value1",
90 "key2": 123,
91 },
92 prefix: "",
93 expected: []string{"key1", "key2"},
94 },
95 {
96 name: "nested map",
97 input: map[string]any{
98 "parent": map[string]any{
99 "child1": "value1",
100 "child2": "value2",
101 },
102 },
103 prefix: "",
104 expected: []string{"parent.child1", "parent.child2"},
105 },
106 {
107 name: "with prefix",
108 input: map[string]any{
109 "key": "value",
110 },
111 prefix: "section",
112 expected: []string{"section.key"},
113 },
114 {
115 name: "search_shortcuts treated as single key",
116 input: map[string]any{
117 "search_shortcuts": map[string]any{
118 "g": "https://google.com/?q=%s",
119 "ddg": "https://duckduckgo.com/?q=%s",
120 },
121 },
122 prefix: "",
123 expected: []string{"search_shortcuts"}, // Should be treated as user data
124 },
125 {
126 name: "actions map treated as single key",
127 input: map[string]any{
128 "pane_mode": map[string]any{
129 "actions": map[string]any{
130 "split-right": []string{"r"},
131 },
132 },
133 },
134 prefix: "",
135 expected: []string{"pane_mode.actions"}, // Actions treated as user data
136 },
137 }
138
139 for _, tt := range tests {
140 t.Run(tt.name, func(t *testing.T) {
141 keys := make(map[string]bool)
142 m.flattenMap(tt.input, tt.prefix, keys)
143
144 for _, expectedKey := range tt.expected {
145 assert.True(t, keys[expectedKey], "should contain key: %s", expectedKey)
146 }
147 })
148 }
149}
150
151func TestMigrator_IsUserDataSection(t *testing.T) {
152 m := NewMigrator()
153
154 tests := []struct {
155 name string
156 keyPath string
157 expected bool
158 }{
159 {
160 name: "search_shortcuts is user data",
161 keyPath: "search_shortcuts",
162 expected: true,
163 },
164 {
165 name: "pane_mode.actions is user data",
166 keyPath: "pane_mode.actions",
167 expected: true,
168 },
169 {
170 name: "tab_mode.actions is user data",
171 keyPath: "tab_mode.actions",
172 expected: true,
173 },
174 {
175 name: "history is not user data",
176 keyPath: "history",
177 expected: false,
178 },
179 {
180 name: "history.max_entries is not user data",
181 keyPath: "history.max_entries",
182 expected: false,
183 },
184 {
185 name: "appearance.sans_font is not user data",
186 keyPath: "appearance.sans_font",
187 expected: false,
188 },
189 }
190
191 for _, tt := range tests {
192 t.Run(tt.name, func(t *testing.T) {
193 result := m.isUserDataSection(tt.keyPath)
194 assert.Equal(t, tt.expected, result)
195 })
196 }
197}
198
199func TestMigrator_FindMissingKeys(t *testing.T) {
200 m := NewMigrator()
201
202 tests := []struct {
203 name string
204 defaultKeys []string
205 userKeys map[string]bool
206 expected []string
207 }{
208 {
209 name: "all keys present",
210 defaultKeys: []string{"key1", "key2"},
211 userKeys: map[string]bool{"key1": true, "key2": true},
212 expected: []string{},
213 },
214 {
215 name: "some keys missing",
216 defaultKeys: []string{"key1", "key2", "key3"},
217 userKeys: map[string]bool{"key1": true},
218 expected: []string{"key2", "key3"},
219 },
220 {
221 name: "all keys missing",
222 defaultKeys: []string{"key1", "key2"},
223 userKeys: map[string]bool{},
224 expected: []string{"key1", "key2"},
225 },
226 {
227 name: "parent key covers children",
228 defaultKeys: []string{"parent.child1", "parent.child2"},
229 userKeys: map[string]bool{"parent": true},
230 expected: []string{}, // Parent covers children
231 },
232 {
233 name: "child keys cover parent (struct defaults like palettes)",
234 defaultKeys: []string{"appearance.dark_palette"},
235 userKeys: map[string]bool{"appearance.dark_palette.background": true, "appearance.dark_palette.accent": true},
236 expected: []string{}, // Child keys mean parent is defined
237 },
238 }
239
240 for _, tt := range tests {
241 t.Run(tt.name, func(t *testing.T) {
242 result := m.findMissingKeys(tt.defaultKeys, tt.userKeys)
243 assert.ElementsMatch(t, tt.expected, result)
244 })
245 }
246}
247
248func TestMigrator_GetKeyInfo(t *testing.T) {
249 m := NewMigrator()
250
251 tests := []struct {
252 name string
253 key string
254 expectedType string
255 }{
256 {
257 name: "bool key",
258 key: "debug.enable_devtools",
259 expectedType: "bool",
260 },
261 {
262 name: "int key",
263 key: "history.max_entries",
264 expectedType: "int",
265 },
266 {
267 name: "string key",
268 key: "logging.level",
269 expectedType: "string",
270 },
271 {
272 name: "float key",
273 key: "default_webpage_zoom",
274 expectedType: "float",
275 },
276 }
277
278 for _, tt := range tests {
279 t.Run(tt.name, func(t *testing.T) {
280 info := m.GetKeyInfo(tt.key)
281 assert.Equal(t, tt.key, info.Key)
282 assert.Equal(t, tt.expectedType, info.Type)
283 assert.NotEmpty(t, info.DefaultValue)
284 })
285 }
286}
287
288func TestMigrator_GetTypeName(t *testing.T) {
289 m := NewMigrator()
290
291 tests := []struct {
292 name string
293 value any
294 expected string
295 }{
296 {name: "bool", value: true, expected: "bool"},
297 {name: "int", value: 42, expected: "int"},
298 {name: "int64", value: int64(42), expected: "int"},
299 {name: "float64", value: 3.14, expected: "float"},
300 {name: "string", value: "hello", expected: "string"},
301 {name: "slice", value: []any{"a", "b"}, expected: "list"},
302 {name: "map", value: map[string]any{"a": 1}, expected: "map"},
303 {name: "nil", value: nil, expected: "unknown"},
304 }
305
306 for _, tt := range tests {
307 t.Run(tt.name, func(t *testing.T) {
308 result := m.getTypeName(tt.value)
309 assert.Equal(t, tt.expected, result)
310 })
311 }
312}
313
314func TestMigrator_FormatValue(t *testing.T) {
315 m := NewMigrator()
316
317 tests := []struct {
318 name string
319 value any
320 expected string
321 }{
322 {name: "true", value: true, expected: "true"},
323 {name: "false", value: false, expected: "false"},
324 {name: "int", value: 42, expected: "42"},
325 {name: "float", value: 3.14, expected: "3.14"},
326 {name: "string", value: "hello", expected: `"hello"`},
327 {name: "empty string", value: "", expected: `""`},
328 {name: "empty slice", value: []any{}, expected: "[]"},
329 {name: "slice with items", value: []any{"a", "b", "c"}, expected: "[3 items]"},
330 {name: "empty map", value: map[string]any{}, expected: "{}"},
331 {name: "map with entries", value: map[string]any{"a": 1, "b": 2}, expected: "{2 entries}"},
332 {name: "nil", value: nil, expected: "null"},
333 }
334
335 for _, tt := range tests {
336 t.Run(tt.name, func(t *testing.T) {
337 result := m.formatValue(tt.value)
338 assert.Equal(t, tt.expected, result)
339 })
340 }
341}
342
343func TestKeysAreSimilar(t *testing.T) {
344 tests := []struct {
345 name string
346 oldKey string
347 newKey string
348 expected bool
349 }{
350 {
351 name: "identical keys",
352 oldKey: "section.pane_mode_color",
353 newKey: "section.pane_mode_color",
354 expected: true,
355 },
356 {
357 name: "substring match - old contains new",
358 oldKey: "section.pane_mode_border_color",
359 newKey: "section.pane_mode_color",
360 expected: true,
361 },
362 {
363 name: "substring match - new contains old",
364 oldKey: "section.mode_color",
365 newKey: "section.pane_mode_color",
366 expected: true,
367 },
368 {
369 name: "token matching - border removed",
370 oldKey: "section.tab_mode_border_color",
371 newKey: "section.tab_mode_color",
372 expected: true,
373 },
374 {
375 name: "different parent paths",
376 oldKey: "section_a.pane_mode_color",
377 newKey: "section_b.pane_mode_color",
378 expected: false,
379 },
380 {
381 name: "different depth",
382 oldKey: "pane_mode_color",
383 newKey: "section.pane_mode_color",
384 expected: false,
385 },
386 {
387 name: "completely different keys",
388 oldKey: "section.foo_bar",
389 newKey: "section.baz_qux",
390 expected: false,
391 },
392 {
393 name: "top level keys rejected",
394 oldKey: "foo",
395 newKey: "bar",
396 expected: false,
397 },
398 {
399 name: "no common tokens",
400 oldKey: "section.aaa_bbb",
401 newKey: "section.ccc_ddd",
402 expected: false,
403 },
404 {
405 name: "single token difference allowed",
406 oldKey: "section.pane_mode_border_width",
407 newKey: "section.mode_border_width",
408 expected: true,
409 },
410 {
411 name: "duplicate tokens in old key - no double counting",
412 oldKey: "section.mode_mode_mode",
413 newKey: "section.mode_other_another",
414 expected: false, // oldTokens=[mode,mode,mode], newTokens=[mode,other,another], matches=1 (only one mode can match), minTokens=3, needs 1>=2 which is false
415 },
416 {
417 name: "enough common tokens with duplicates",
418 oldKey: "section.pane_mode_border",
419 newKey: "section.pane_mode_color",
420 expected: true, // oldTokens=[pane,mode,border], newTokens=[pane,mode,color], matches=2, minTokens=3, 2>=2 && 2>0 = true
421 },
422 }
423
424 for _, tt := range tests {
425 t.Run(tt.name, func(t *testing.T) {
426 result := keysAreSimilar(tt.oldKey, tt.newKey)
427 assert.Equal(t, tt.expected, result, "keysAreSimilar(%q, %q)", tt.oldKey, tt.newKey)
428 })
429 }
430}
431
432func TestMigrator_TypesAreCompatible(t *testing.T) {
433 m := NewMigrator()
434
435 tests := []struct {
436 name string
437 oldKey string
438 newKey string
439 expected bool
440 }{
441 {
442 name: "color to color - compatible (using real key)",
443 oldKey: "workspace.styling.pane_mode_border_color",
444 newKey: "workspace.styling.pane_mode_color",
445 expected: true,
446 },
447 {
448 name: "width to width - compatible (using real key)",
449 oldKey: "workspace.styling.pane_mode_border_width",
450 newKey: "workspace.styling.mode_border_width",
451 expected: true,
452 },
453 {
454 name: "width suffix to color type - incompatible",
455 oldKey: "workspace.styling.some_width",
456 newKey: "workspace.styling.pane_mode_color", // real key with string type
457 expected: false,
458 },
459 {
460 name: "color suffix to int type - incompatible",
461 oldKey: "workspace.styling.some_color",
462 newKey: "workspace.styling.mode_border_width", // real key with int type
463 expected: false,
464 },
465 {
466 name: "generic key to string type - compatible",
467 oldKey: "workspace.styling.some_setting",
468 newKey: "workspace.styling.pane_mode_color", // real key with string type
469 expected: true,
470 },
471 {
472 name: "non-existent new key - incompatible",
473 oldKey: "section.old_key",
474 newKey: "section.nonexistent_key_xyz",
475 expected: false,
476 },
477 }
478
479 for _, tt := range tests {
480 t.Run(tt.name, func(t *testing.T) {
481 result := m.typesAreCompatible(tt.oldKey, tt.newKey)
482 assert.Equal(t, tt.expected, result, "typesAreCompatible(%q, %q)", tt.oldKey, tt.newKey)
483 })
484 }
485}
486
487func TestMigrator_FindDeprecatedKeys(t *testing.T) {
488 m := NewMigrator()
489
490 tests := []struct {
491 name string
492 userKeys map[string]any
493 defaultKeys map[string]bool
494 expected []string
495 }{
496 {
497 name: "no deprecated keys",
498 userKeys: map[string]any{
499 "history.max_entries": 1000,
500 "logging.level": "info",
501 },
502 defaultKeys: map[string]bool{
503 "history.max_entries": true,
504 "logging.level": true,
505 },
506 expected: []string{},
507 },
508 {
509 name: "one deprecated key",
510 userKeys: map[string]any{
511 "history.max_entries": 1000,
512 "old.deprecated_key": "value",
513 },
514 defaultKeys: map[string]bool{
515 "history.max_entries": true,
516 },
517 expected: []string{"old.deprecated_key"},
518 },
519 {
520 name: "multiple deprecated keys",
521 userKeys: map[string]any{
522 "old.key1": "a",
523 "old.key2": "b",
524 "old.key3": "c",
525 },
526 defaultKeys: map[string]bool{
527 "new.key1": true,
528 },
529 expected: []string{"old.key1", "old.key2", "old.key3"},
530 },
531 {
532 name: "parent key exists in defaults",
533 userKeys: map[string]any{
534 "parent.child": "value",
535 },
536 defaultKeys: map[string]bool{
537 "parent.child": true,
538 "parent.other_child": true,
539 },
540 expected: []string{},
541 },
542 }
543
544 for _, tt := range tests {
545 t.Run(tt.name, func(t *testing.T) {
546 result := m.findDeprecatedKeys(tt.userKeys, tt.defaultKeys)
547 assert.ElementsMatch(t, tt.expected, result)
548 })
549 }
550}
551
552func TestMigrator_MatchRenamedKeys(t *testing.T) {
553 m := NewMigrator()
554
555 tests := []struct {
556 name string
557 deprecated []string
558 missing []string
559 expectedRenames map[string]string
560 expectedUnmatchedOld []string
561 expectedUnmatchedNew []string
562 }{
563 {
564 name: "no keys to match",
565 deprecated: []string{},
566 missing: []string{},
567 expectedRenames: map[string]string{},
568 expectedUnmatchedOld: nil,
569 expectedUnmatchedNew: nil,
570 },
571 {
572 name: "simple rename match (using real keys)",
573 deprecated: []string{"workspace.styling.pane_mode_border_color"},
574 missing: []string{"workspace.styling.pane_mode_color"},
575 expectedRenames: map[string]string{
576 "workspace.styling.pane_mode_border_color": "workspace.styling.pane_mode_color",
577 },
578 expectedUnmatchedOld: nil,
579 expectedUnmatchedNew: nil,
580 },
581 {
582 name: "no match possible - different types (width vs color)",
583 deprecated: []string{"workspace.styling.pane_mode_border_width"},
584 missing: []string{"workspace.styling.pane_mode_color"},
585 expectedRenames: map[string]string{},
586 expectedUnmatchedOld: []string{"workspace.styling.pane_mode_border_width"},
587 expectedUnmatchedNew: []string{"workspace.styling.pane_mode_color"},
588 },
589 {
590 name: "no match possible - different parents",
591 deprecated: []string{"old_section.some_key"},
592 missing: []string{"new_section.some_key"},
593 expectedRenames: map[string]string{},
594 expectedUnmatchedOld: []string{"old_section.some_key"},
595 expectedUnmatchedNew: []string{"new_section.some_key"},
596 },
597 {
598 name: "multiple renames (using real keys)",
599 deprecated: []string{"workspace.styling.pane_mode_border_color", "workspace.styling.tab_mode_border_color"},
600 missing: []string{"workspace.styling.pane_mode_color", "workspace.styling.tab_mode_color"},
601 expectedRenames: map[string]string{
602 "workspace.styling.pane_mode_border_color": "workspace.styling.pane_mode_color",
603 "workspace.styling.tab_mode_border_color": "workspace.styling.tab_mode_color",
604 },
605 expectedUnmatchedOld: nil,
606 expectedUnmatchedNew: nil,
607 },
608 }
609
610 for _, tt := range tests {
611 t.Run(tt.name, func(t *testing.T) {
612 renames, unmatchedOld, unmatchedNew := m.matchRenamedKeys(tt.deprecated, tt.missing)
613 assert.Equal(t, tt.expectedRenames, renames, "renames mismatch")
614 assert.ElementsMatch(t, tt.expectedUnmatchedOld, unmatchedOld, "unmatched old mismatch")
615 assert.ElementsMatch(t, tt.expectedUnmatchedNew, unmatchedNew, "unmatched new mismatch")
616 })
617 }
618}
619
620func TestMigrator_CheckMigration_NoConfigFile(t *testing.T) {
621 // This test would need to temporarily modify the config path
622 // which is difficult without refactoring. Skip for now.
623 t.Skip("requires config path mocking")
624}
625
626func TestMigrator_Migrate_NoConfigFile(t *testing.T) {
627 // This test would need to temporarily modify the config path
628 // which is difficult without refactoring. Skip for now.
629 t.Skip("requires config path mocking")
630}
631
632func TestMigrator_GetConfigType(t *testing.T) {
633 m := NewMigrator()
634
635 tests := []struct {
636 name string
637 configFile string
638 expected string
639 }{
640 {
641 name: "toml file",
642 configFile: "/path/to/config.toml",
643 expected: "toml",
644 },
645 {
646 name: "yaml file",
647 configFile: "/path/to/config.yaml",
648 expected: "yaml",
649 },
650 {
651 name: "yml file",
652 configFile: "/path/to/config.yml",
653 expected: "yaml",
654 },
655 {
656 name: "json file",
657 configFile: "/path/to/config.json",
658 expected: "json",
659 },
660 {
661 name: "unknown extension defaults to toml",
662 configFile: "/path/to/config.unknown",
663 expected: "toml",
664 },
665 {
666 name: "no extension defaults to toml",
667 configFile: "/path/to/config",
668 expected: "toml",
669 },
670 }
671
672 for _, tt := range tests {
673 t.Run(tt.name, func(t *testing.T) {
674 result := m.getConfigType(tt.configFile)
675 assert.Equal(t, tt.expected, result)
676 })
677 }
678}
679
680func TestMigrator_GetNestedValue(t *testing.T) {
681 m := NewMigrator()
682
683 config := map[string]any{
684 "level1": map[string]any{
685 "level2": map[string]any{
686 "level3": "deep_value",
687 },
688 "simple": "simple_value",
689 },
690 "top": "top_value",
691 }
692
693 tests := []struct {
694 name string
695 key string
696 expected any
697 }{
698 {
699 name: "top level key",
700 key: "top",
701 expected: "top_value",
702 },
703 {
704 name: "nested key",
705 key: "level1.simple",
706 expected: "simple_value",
707 },
708 {
709 name: "deeply nested key",
710 key: "level1.level2.level3",
711 expected: "deep_value",
712 },
713 {
714 name: "non-existent key",
715 key: "nonexistent",
716 expected: nil,
717 },
718 {
719 name: "non-existent nested key",
720 key: "level1.nonexistent",
721 expected: nil,
722 },
723 }
724
725 for _, tt := range tests {
726 t.Run(tt.name, func(t *testing.T) {
727 result := m.getNestedValue(config, tt.key)
728 assert.Equal(t, tt.expected, result)
729 })
730 }
731}
732
733func TestMigrator_SetNestedValue(t *testing.T) {
734 m := NewMigrator()
735
736 t.Run("set top level value", func(t *testing.T) {
737 config := make(map[string]any)
738 m.setNestedValue(config, "top", "value")
739 assert.Equal(t, "value", config["top"])
740 })
741
742 t.Run("set nested value with existing parent", func(t *testing.T) {
743 config := map[string]any{
744 "parent": map[string]any{},
745 }
746 m.setNestedValue(config, "parent.child", "value")
747 parent := config["parent"].(map[string]any)
748 assert.Equal(t, "value", parent["child"])
749 })
750
751 t.Run("set nested value creates intermediate maps", func(t *testing.T) {
752 config := make(map[string]any)
753 m.setNestedValue(config, "a.b.c", "deep_value")
754
755 a := config["a"].(map[string]any)
756 b := a["b"].(map[string]any)
757 assert.Equal(t, "deep_value", b["c"])
758 })
759}
760
761func TestMigrator_DeleteNestedKey(t *testing.T) {
762 m := NewMigrator()
763
764 t.Run("delete top level key", func(t *testing.T) {
765 config := map[string]any{
766 "keep": "value",
767 "delete": "value",
768 }
769 m.deleteNestedKey(config, "delete")
770 assert.Contains(t, config, "keep")
771 assert.NotContains(t, config, "delete")
772 })
773
774 t.Run("delete nested key", func(t *testing.T) {
775 config := map[string]any{
776 "parent": map[string]any{
777 "keep": "value",
778 "delete": "value",
779 },
780 }
781 m.deleteNestedKey(config, "parent.delete")
782 parent := config["parent"].(map[string]any)
783 assert.Contains(t, parent, "keep")
784 assert.NotContains(t, parent, "delete")
785 })
786
787 t.Run("delete nested key cleans up empty parents", func(t *testing.T) {
788 config := map[string]any{
789 "keep": "value",
790 "parent": map[string]any{
791 "child": map[string]any{
792 "leaf": "value",
793 },
794 },
795 }
796 m.deleteNestedKey(config, "parent.child.leaf")
797 // Both parent and child should be removed since they're empty
798 assert.NotContains(t, config, "parent")
799 assert.Contains(t, config, "keep")
800 })
801
802 t.Run("delete non-existent key is no-op", func(t *testing.T) {
803 config := map[string]any{
804 "keep": "value",
805 }
806 m.deleteNestedKey(config, "nonexistent")
807 assert.Contains(t, config, "keep")
808 })
809}
810
811func TestMigrator_ReadRawConfig(t *testing.T) {
812 m := NewMigrator()
813
814 t.Run("read TOML config", func(t *testing.T) {
815 tmpDir := t.TempDir()
816 configFile := filepath.Join(tmpDir, "config.toml")
817
818 content := `
819[section]
820key = "value"
821number = 42
822`
823 err := os.WriteFile(configFile, []byte(content), 0o644)
824 require.NoError(t, err)
825
826 config, err := m.readRawConfig(configFile)
827 require.NoError(t, err)
828
829 section := config["section"].(map[string]any)
830 assert.Equal(t, "value", section["key"])
831 assert.Equal(t, int64(42), section["number"])
832 })
833
834 t.Run("read YAML config", func(t *testing.T) {
835 tmpDir := t.TempDir()
836 configFile := filepath.Join(tmpDir, "config.yaml")
837
838 content := `
839section:
840 key: value
841 number: 42
842`
843 err := os.WriteFile(configFile, []byte(content), 0o644)
844 require.NoError(t, err)
845
846 config, err := m.readRawConfig(configFile)
847 require.NoError(t, err)
848
849 section := config["section"].(map[string]any)
850 assert.Equal(t, "value", section["key"])
851 assert.Equal(t, 42, section["number"])
852 })
853
854 t.Run("read JSON config", func(t *testing.T) {
855 tmpDir := t.TempDir()
856 configFile := filepath.Join(tmpDir, "config.json")
857
858 content := `{
859 "section": {
860 "key": "value",
861 "number": 42
862 }
863}`
864 err := os.WriteFile(configFile, []byte(content), 0o644)
865 require.NoError(t, err)
866
867 config, err := m.readRawConfig(configFile)
868 require.NoError(t, err)
869
870 section := config["section"].(map[string]any)
871 assert.Equal(t, "value", section["key"])
872 // JSON numbers are float64
873 assert.InDelta(t, 42.0, section["number"].(float64), 0.001)
874 })
875}