1package usecase_test
2
3import (
4 "errors"
5 "testing"
6
7 "github.com/bnema/dumber/internal/application/dto"
8 "github.com/bnema/dumber/internal/application/usecase"
9 "github.com/bnema/dumber/internal/domain/entity"
10 repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14)
15
16func TestManageFavoritesUseCase_GetAllURLs_ReturnsURLSet(t *testing.T) {
17 ctx := testContext()
18
19 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
20 folderRepo := repomocks.NewMockFolderRepository(t)
21 tagRepo := repomocks.NewMockTagRepository(t)
22
23 favorites := []*entity.Favorite{
24 {ID: 1, URL: "https://example.com"},
25 {ID: 2, URL: "https://github.com"},
26 {ID: 3, URL: "https://google.com"},
27 }
28
29 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
30
31 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
32
33 urls, err := uc.GetAllURLs(ctx)
34 require.NoError(t, err)
35 require.NotNil(t, urls)
36
37 assert.Len(t, urls, 3)
38 _, hasExample := urls["https://example.com"]
39 _, hasGithub := urls["https://github.com"]
40 _, hasGoogle := urls["https://google.com"]
41 assert.True(t, hasExample)
42 assert.True(t, hasGithub)
43 assert.True(t, hasGoogle)
44}
45
46func TestManageFavoritesUseCase_GetAllURLs_ReturnsEmptySetWhenNoFavorites(t *testing.T) {
47 ctx := testContext()
48
49 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
50 folderRepo := repomocks.NewMockFolderRepository(t)
51 tagRepo := repomocks.NewMockTagRepository(t)
52
53 favoriteRepo.EXPECT().GetAll(mock.Anything).Return([]*entity.Favorite{}, nil)
54
55 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
56
57 urls, err := uc.GetAllURLs(ctx)
58 require.NoError(t, err)
59 require.NotNil(t, urls)
60 assert.Empty(t, urls)
61}
62
63func TestManageFavoritesUseCase_GetAllURLs_ReturnsErrorOnRepoFailure(t *testing.T) {
64 ctx := testContext()
65
66 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
67 folderRepo := repomocks.NewMockFolderRepository(t)
68 tagRepo := repomocks.NewMockTagRepository(t)
69
70 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(nil, errors.New("db connection failed"))
71
72 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
73
74 urls, err := uc.GetAllURLs(ctx)
75 require.Error(t, err)
76 assert.Nil(t, urls)
77 assert.Contains(t, err.Error(), "failed to get favorites")
78}
79
80func TestManageFavoritesUseCase_Toggle_AddsWhenNotFavorite(t *testing.T) {
81 ctx := testContext()
82
83 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
84 folderRepo := repomocks.NewMockFolderRepository(t)
85 tagRepo := repomocks.NewMockTagRepository(t)
86
87 // URL is not a favorite
88 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil)
89 favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(f *entity.Favorite) bool {
90 return f.URL == "https://example.com" && f.Title == "Example"
91 })).Return(nil)
92
93 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
94
95 result, err := uc.Toggle(ctx, "https://example.com", "Example")
96 require.NoError(t, err)
97 require.NotNil(t, result)
98
99 assert.True(t, result.Added)
100 assert.Equal(t, "https://example.com", result.URL)
101 assert.Equal(t, "Example", result.Title)
102 assert.Equal(t, "Favorite added", result.Message)
103}
104
105func TestManageFavoritesUseCase_Toggle_RemovesWhenAlreadyFavorite(t *testing.T) {
106 ctx := testContext()
107
108 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
109 folderRepo := repomocks.NewMockFolderRepository(t)
110 tagRepo := repomocks.NewMockTagRepository(t)
111
112 existingFav := &entity.Favorite{
113 ID: 42,
114 URL: "https://example.com",
115 Title: "Example Site",
116 }
117
118 // URL is already a favorite
119 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)
120 favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(nil)
121
122 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
123
124 result, err := uc.Toggle(ctx, "https://example.com", "Example")
125 require.NoError(t, err)
126 require.NotNil(t, result)
127
128 assert.False(t, result.Added)
129 assert.Equal(t, "https://example.com", result.URL)
130 assert.Equal(t, "Example Site", result.Title) // Uses existing title
131 assert.Equal(t, "Favorite removed", result.Message)
132}
133
134func TestManageFavoritesUseCase_Toggle_EmptyURLReturnsError(t *testing.T) {
135 ctx := testContext()
136
137 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
138 folderRepo := repomocks.NewMockFolderRepository(t)
139 tagRepo := repomocks.NewMockTagRepository(t)
140
141 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
142
143 result, err := uc.Toggle(ctx, "", "Title")
144 require.Error(t, err)
145 assert.Nil(t, result)
146 assert.Contains(t, err.Error(), "favorite URL is required")
147}
148
149func TestManageFavoritesUseCase_ToggleNormalizesURLBeforeLookup(t *testing.T) {
150 ctx := testContext()
151
152 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
153 folderRepo := repomocks.NewMockFolderRepository(t)
154 tagRepo := repomocks.NewMockTagRepository(t)
155
156 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()
157 favoriteRepo.EXPECT().FindByURL(mock.Anything, "example.com").Return(nil, nil).Once()
158 favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(fav *entity.Favorite) bool {
159 return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example"
160 })).Return(nil).Once()
161
162 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
163
164 result, err := uc.Toggle(ctx, " example.com ", " Example ")
165 require.NoError(t, err)
166 require.NotNil(t, result)
167 assert.True(t, result.Added)
168 assert.Equal(t, "https://example.com", result.URL)
169 assert.Equal(t, "Example", result.Title)
170}
171
172func TestManageFavoritesUseCase_Toggle_FindByURLErrorReturnsError(t *testing.T) {
173 ctx := testContext()
174
175 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
176 folderRepo := repomocks.NewMockFolderRepository(t)
177 tagRepo := repomocks.NewMockTagRepository(t)
178
179 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, errors.New("db error"))
180
181 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
182
183 result, err := uc.Toggle(ctx, "https://example.com", "Example")
184 require.Error(t, err)
185 assert.Nil(t, result)
186 assert.Contains(t, err.Error(), "failed to check existing favorite")
187}
188
189func TestManageFavoritesUseCase_Toggle_SaveErrorReturnsError(t *testing.T) {
190 ctx := testContext()
191
192 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
193 folderRepo := repomocks.NewMockFolderRepository(t)
194 tagRepo := repomocks.NewMockTagRepository(t)
195
196 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil)
197 favoriteRepo.EXPECT().Save(mock.Anything, mock.Anything).Return(errors.New("db error"))
198
199 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
200
201 result, err := uc.Toggle(ctx, "https://example.com", "Example")
202 require.Error(t, err)
203 assert.Nil(t, result)
204 assert.Contains(t, err.Error(), "failed to add favorite")
205}
206
207func TestManageFavoritesUseCase_Toggle_DeleteErrorReturnsError(t *testing.T) {
208 ctx := testContext()
209
210 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
211 folderRepo := repomocks.NewMockFolderRepository(t)
212 tagRepo := repomocks.NewMockTagRepository(t)
213
214 existingFav := &entity.Favorite{ID: 42, URL: "https://example.com", Title: "Example"}
215 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)
216 favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(errors.New("db error"))
217
218 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
219
220 result, err := uc.Toggle(ctx, "https://example.com", "Example")
221 require.Error(t, err)
222 assert.Nil(t, result)
223 assert.Contains(t, err.Error(), "failed to remove favorite")
224}
225
226func TestManageFavoritesUseCase_FilterForOmnibox_RanksPrefixBeforeContains(t *testing.T) {
227 ctx := testContext()
228
229 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
230 folderRepo := repomocks.NewMockFolderRepository(t)
231 tagRepo := repomocks.NewMockTagRepository(t)
232
233 favorites := []*entity.Favorite{
234 {ID: 1, URL: "https://example.com/landing", Title: "GitHub docs mirror", Position: 0},
235 {ID: 2, URL: "https://github.com/bnema/dumber", Title: "Project repo", Position: 1},
236 {ID: 3, URL: "https://gitlab.com/team/project", Title: "GitLab", Position: 2},
237 }
238 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
239
240 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
241
242 results, err := uc.FilterForOmnibox(ctx, "git")
243 require.NoError(t, err)
244 require.Len(t, results, 3)
245 assert.Equal(t, "https://github.com/bnema/dumber", results[0].URL)
246 assert.Equal(t, "https://gitlab.com/team/project", results[1].URL)
247 assert.Equal(t, "https://example.com/landing", results[2].URL)
248}
249
250func TestManageFavoritesUseCase_FilterForOmnibox_EmptyQueryKeepsRepositoryOrder(t *testing.T) {
251 ctx := testContext()
252
253 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
254 folderRepo := repomocks.NewMockFolderRepository(t)
255 tagRepo := repomocks.NewMockTagRepository(t)
256
257 favorites := []*entity.Favorite{
258 {ID: 1, URL: "https://first.example"},
259 {ID: 2, URL: "https://second.example"},
260 }
261 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
262
263 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
264
265 results, err := uc.FilterForOmnibox(ctx, "")
266 require.NoError(t, err)
267 require.Len(t, results, 2)
268 assert.Equal(t, "https://first.example", results[0].URL)
269 assert.Equal(t, "https://second.example", results[1].URL)
270}
271
272func TestManageFavoritesUseCase_GetAllURLs_UsesCache(t *testing.T) {
273 ctx := testContext()
274
275 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
276 folderRepo := repomocks.NewMockFolderRepository(t)
277 tagRepo := repomocks.NewMockTagRepository(t)
278
279 favorites := []*entity.Favorite{
280 {ID: 1, URL: "https://example.com"},
281 }
282 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil).Once()
283
284 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
285
286 urls, err := uc.GetAllURLs(ctx)
287 require.NoError(t, err)
288 require.Contains(t, urls, "https://example.com")
289
290 urls, err = uc.GetAllURLs(ctx)
291 require.NoError(t, err)
292 require.Contains(t, urls, "https://example.com")
293}
294
295func TestManageFavoritesUseCase_Toggle_InvalidatesCache(t *testing.T) {
296 ctx := testContext()
297
298 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
299 folderRepo := repomocks.NewMockFolderRepository(t)
300 tagRepo := repomocks.NewMockTagRepository(t)
301
302 initialFavorites := []*entity.Favorite{
303 {ID: 1, URL: "https://cached.example"},
304 }
305 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(initialFavorites, nil).Once()
306 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://new.example").Return(nil, nil)
307 favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(f *entity.Favorite) bool {
308 return f.URL == "https://new.example"
309 })).Return(nil)
310 updatedFavorites := []*entity.Favorite{
311 {ID: 1, URL: "https://cached.example"},
312 {ID: 2, URL: "https://new.example"},
313 }
314 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(updatedFavorites, nil).Once()
315
316 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
317
318 urls, err := uc.GetAllURLs(ctx)
319 require.NoError(t, err)
320 require.Contains(t, urls, "https://cached.example")
321
322 _, err = uc.Toggle(ctx, "https://new.example", "New")
323 require.NoError(t, err)
324
325 urls, err = uc.GetAllURLs(ctx)
326 require.NoError(t, err)
327 require.Contains(t, urls, "https://new.example")
328}
329
330func TestManageFavoritesUseCase_DeleteFolder_InvalidatesFavoritesCache(t *testing.T) {
331 ctx := testContext()
332
333 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
334 folderRepo := repomocks.NewMockFolderRepository(t)
335 tagRepo := repomocks.NewMockTagRepository(t)
336
337 folderID := entity.FolderID(9)
338 initialFolderID := folderID
339 cachedFavorites := []*entity.Favorite{
340 {ID: 1, URL: "https://example.com", FolderID: &initialFolderID},
341 }
342 updatedFavorites := []*entity.Favorite{
343 {ID: 1, URL: "https://example.com", FolderID: nil},
344 }
345
346 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(cachedFavorites, nil).Once()
347 folderRepo.EXPECT().FindByID(mock.Anything, folderID).Return(&entity.Folder{ID: folderID}, nil).Once()
348 favoriteRepo.EXPECT().GetByFolder(mock.Anything, &folderID).Return(cachedFavorites, nil).Once()
349 favoriteRepo.EXPECT().SetFolder(mock.Anything, entity.FavoriteID(1), (*entity.FolderID)(nil)).Return(nil).Once()
350 folderRepo.EXPECT().GetChildren(mock.Anything, &folderID).Return([]*entity.Folder{}, nil).Once()
351 folderRepo.EXPECT().Delete(mock.Anything, folderID).Return(nil).Once()
352 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(updatedFavorites, nil).Once()
353
354 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
355
356 favoritesBefore, err := uc.GetAll(ctx)
357 require.NoError(t, err)
358 require.Len(t, favoritesBefore, 1)
359 require.NotNil(t, favoritesBefore[0].FolderID)
360
361 err = uc.DeleteFolder(ctx, folderID)
362 require.NoError(t, err)
363
364 favoritesAfter, err := uc.GetAll(ctx)
365 require.NoError(t, err)
366 require.Len(t, favoritesAfter, 1)
367 assert.Nil(t, favoritesAfter[0].FolderID)
368}
369
370func TestManageFavoritesUseCase_AddFavoriteNormalizesURL(t *testing.T) {
371 ctx := testContext()
372
373 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
374 folderRepo := repomocks.NewMockFolderRepository(t)
375 tagRepo := repomocks.NewMockTagRepository(t)
376
377 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()
378 favoriteRepo.EXPECT().FindByURL(mock.Anything, "example.com").Return(nil, nil).Once()
379 favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(fav *entity.Favorite) bool {
380 return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example"
381 })).Return(nil).Once()
382
383 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
384
385 fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "example.com", Title: " Example "})
386 require.NoError(t, err)
387 require.NotNil(t, fav)
388 assert.Equal(t, "https://example.com", fav.URL)
389 assert.Equal(t, "Example", fav.Title)
390}
391
392func TestManageFavoritesUseCase_AddFavoriteRejectsInvalidTagIDs(t *testing.T) {
393 ctx := testContext()
394
395 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
396 folderRepo := repomocks.NewMockFolderRepository(t)
397 tagRepo := repomocks.NewMockTagRepository(t)
398 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
399
400 _, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "https://example.com", Tags: []entity.TagID{1, 0}})
401 require.Error(t, err)
402 assert.Contains(t, err.Error(), "favorite tag id must be positive")
403}
404
405func TestManageFavoritesUseCase_AddFavoriteRejectsUnsafeOrMalformedURL(t *testing.T) {
406 ctx := testContext()
407
408 tests := []string{
409 "",
410 "not a url",
411 "javascript:alert(1)",
412 "data:text/html,<script>alert(1)</script>",
413 "https:///missing-host",
414 "dumb:///missing-host",
415 }
416 for _, raw := range tests {
417 t.Run(raw, func(t *testing.T) {
418 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
419 folderRepo := repomocks.NewMockFolderRepository(t)
420 tagRepo := repomocks.NewMockTagRepository(t)
421 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
422
423 _, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: raw})
424 require.Error(t, err)
425 })
426 }
427}
428
429func TestManageFavoritesUseCase_AddFavoriteNormalizesOpaqueDumbRoute(t *testing.T) {
430 ctx := testContext()
431
432 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
433 folderRepo := repomocks.NewMockFolderRepository(t)
434 tagRepo := repomocks.NewMockTagRepository(t)
435
436 favoriteRepo.EXPECT().FindByURL(mock.Anything, "dumb://history").Return(nil, nil).Once()
437 favoriteRepo.EXPECT().FindByURL(mock.Anything, "dumb:history").Return(nil, nil).Once()
438 favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(fav *entity.Favorite) bool {
439 return fav != nil && fav.URL == "dumb://history" && fav.Title == "History"
440 })).Return(nil).Once()
441
442 uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
443
444 fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "dumb:history", Title: "History"})
445 require.NoError(t, err)
446 require.NotNil(t, fav)
447 assert.Equal(t, "dumb://history", fav.URL)
448}