v0.30.0 eb3068f

manage_favorites_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close