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