1package usecase_test
2
3import (
4 "context"
5 "math"
6 "testing"
7
8 "github.com/bnema/dumber/internal/application/dto"
9 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
10 "github.com/bnema/dumber/internal/application/usecase"
11 "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13)
14
15func TestSaveWebUIConfigUseCase_Validation(t *testing.T) {
16 tests := []struct {
17 name string
18 mutate func(*dto.WebUIConfig)
19 wantErr string
20 }{
21 {
22 name: "rejects invalid default search URL",
23 mutate: func(cfg *dto.WebUIConfig) {
24 cfg.DefaultSearchEngine = "not-a-url-%s"
25 },
26 wantErr: "default_search_engine",
27 },
28 {
29 name: "rejects invalid performance profile",
30 mutate: func(cfg *dto.WebUIConfig) {
31 cfg.Performance.Profile = "turbo"
32 },
33 wantErr: "performance.profile",
34 },
35 {
36 name: "rejects NaN UI scale",
37 mutate: func(cfg *dto.WebUIConfig) {
38 cfg.DefaultUIScale = math.NaN()
39 },
40 wantErr: "default_ui_scale must be a finite value",
41 },
42 {
43 name: "rejects infinite UI scale",
44 mutate: func(cfg *dto.WebUIConfig) {
45 cfg.DefaultUIScale = math.Inf(1)
46 },
47 wantErr: "default_ui_scale must be a finite value",
48 },
49 }
50
51 for _, tt := range tests {
52 t.Run(tt.name, func(t *testing.T) {
53 cfg := validWebUIConfig()
54 tt.mutate(&cfg)
55 saver := portmocks.NewMockWebUIConfigSaver(t)
56 uc := usecase.NewSaveWebUIConfigUseCase(saver)
57
58 err := uc.Execute(context.Background(), cfg)
59
60 require.Error(t, err)
61 require.Contains(t, err.Error(), tt.wantErr)
62 })
63 }
64}
65
66func TestSaveWebUIConfigUseCase_RejectsDuplicateTrimmedSearchShortcutKeys(t *testing.T) {
67 saver := portmocks.NewMockWebUIConfigSaver(t)
68 uc := usecase.NewSaveWebUIConfigUseCase(saver)
69 cfg := validWebUIConfig()
70 cfg.SearchShortcuts["ddg"] = dto.SearchShortcut{URL: "https://duckduckgo.com/?q=%s", Description: "DuckDuckGo"}
71 cfg.SearchShortcuts[" ddg "] = dto.SearchShortcut{URL: "https://example.com/?q=%s", Description: "Duplicate"}
72
73 err := uc.Execute(context.Background(), cfg)
74
75 require.Error(t, err)
76 require.Contains(t, err.Error(), "duplicates")
77}
78
79func TestSaveWebUIConfigUseCase_NormalizesAndSavesValidConfig(t *testing.T) {
80 saver := portmocks.NewMockWebUIConfigSaver(t)
81 var saved dto.WebUIConfig
82 saver.EXPECT().SaveWebUIConfig(mock.Anything, mock.AnythingOfType("dto.WebUIConfig")).Run(func(_ context.Context, cfg dto.WebUIConfig) {
83 saved = cfg
84 }).Return(nil).Once()
85 uc := usecase.NewSaveWebUIConfigUseCase(saver)
86 cfg := validWebUIConfig()
87 cfg.SearchShortcuts[" ddg "] = dto.SearchShortcut{URL: " https://duckduckgo.com/?q=%s ", Description: " DuckDuckGo "}
88 cfg.SearchShortcuts[" "] = dto.SearchShortcut{URL: "https://example.com?q=%s", Description: "Example"}
89
90 err := uc.Execute(context.Background(), cfg)
91
92 require.NoError(t, err)
93 require.NotContains(t, saved.SearchShortcuts, " ddg ")
94 require.NotContains(t, saved.SearchShortcuts, "")
95 require.Contains(t, saved.SearchShortcuts, "ddg")
96 require.Equal(t, "https://duckduckgo.com/?q=%s", saved.SearchShortcuts["ddg"].URL)
97 require.Equal(t, "DuckDuckGo", saved.SearchShortcuts["ddg"].Description)
98}
99
100func validWebUIConfig() dto.WebUIConfig {
101 palette := dto.ColorPalette{
102 Background: "#ffffff",
103 Surface: "#f8f8f8",
104 SurfaceVariant: "#eeeeee",
105 Text: "#111111",
106 Muted: "#666666",
107 Accent: "#0055ff",
108 Border: "#dddddd",
109 }
110 return dto.WebUIConfig{
111 Appearance: dto.WebUIAppearanceConfig{
112 SansFont: "Inter",
113 SerifFont: "Georgia",
114 MonospaceFont: "JetBrains Mono",
115 GtkFont: "Adwaita Sans",
116 DefaultFontSize: 16,
117 ColorScheme: "prefer-dark",
118 LightPalette: palette,
119 DarkPalette: palette,
120 },
121 Performance: dto.WebUIPerformanceConfig{
122 Profile: "default",
123 },
124 DefaultUIScale: 1,
125 DefaultSearchEngine: "https://duckduckgo.com/?q=%s",
126 SearchShortcuts: map[string]dto.SearchShortcut{},
127 }
128}