v0.28.0 3cb3770

manage_favorites_test.go RawHistoryFolder

1package usecase_test
2
3import (
4	"errors"
5	"testing"
6
7	"github.com/bnema/dumber/internal/application/usecase"
8	"github.com/bnema/dumber/internal/domain/entity"
9	repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/mock"
12	"github.com/stretchr/testify/require"
13)
14
15func TestManageFavoritesUseCase_GetAllURLs_ReturnsURLSet(t *testing.T) {
16	ctx := testContext()
17
18	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
19	folderRepo := repomocks.NewMockFolderRepository(t)
20	tagRepo := repomocks.NewMockTagRepository(t)
21
22	favorites := []*entity.Favorite{
23		{ID: 1, URL: "https://example.com"},
24		{ID: 2, URL: "https://github.com"},
25		{ID: 3, URL: "https://google.com"},
26	}
27
28	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
29
30	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
31
32	urls, err := uc.GetAllURLs(ctx)
33	require.NoError(t, err)
34	require.NotNil(t, urls)
35
36	assert.Len(t, urls, 3)
37	_, hasExample := urls["https://example.com"]
38	_, hasGithub := urls["https://github.com"]
39	_, hasGoogle := urls["https://google.com"]
40	assert.True(t, hasExample)
41	assert.True(t, hasGithub)
42	assert.True(t, hasGoogle)
43}
44
45func TestManageFavoritesUseCase_GetAllURLs_ReturnsEmptySetWhenNoFavorites(t *testing.T) {
46	ctx := testContext()
47
48	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
49	folderRepo := repomocks.NewMockFolderRepository(t)
50	tagRepo := repomocks.NewMockTagRepository(t)
51
52	favoriteRepo.EXPECT().GetAll(mock.Anything).Return([]*entity.Favorite{}, nil)
53
54	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, 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	folderRepo := repomocks.NewMockFolderRepository(t)
67	tagRepo := repomocks.NewMockTagRepository(t)
68
69	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(nil, errors.New("db connection failed"))
70
71	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
72
73	urls, err := uc.GetAllURLs(ctx)
74	require.Error(t, err)
75	assert.Nil(t, urls)
76	assert.Contains(t, err.Error(), "failed to get favorites")
77}
78
79func TestManageFavoritesUseCase_Toggle_AddsWhenNotFavorite(t *testing.T) {
80	ctx := testContext()
81
82	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
83	folderRepo := repomocks.NewMockFolderRepository(t)
84	tagRepo := repomocks.NewMockTagRepository(t)
85
86	// URL is not a favorite
87	favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil)
88	favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(f *entity.Favorite) bool {
89		return f.URL == "https://example.com" && f.Title == "Example"
90	})).Return(nil)
91
92	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
93
94	result, err := uc.Toggle(ctx, "https://example.com", "Example")
95	require.NoError(t, err)
96	require.NotNil(t, result)
97
98	assert.True(t, result.Added)
99	assert.Equal(t, "https://example.com", result.URL)
100	assert.Equal(t, "Example", result.Title)
101	assert.Equal(t, "Favorite added", result.Message)
102}
103
104func TestManageFavoritesUseCase_Toggle_RemovesWhenAlreadyFavorite(t *testing.T) {
105	ctx := testContext()
106
107	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
108	folderRepo := repomocks.NewMockFolderRepository(t)
109	tagRepo := repomocks.NewMockTagRepository(t)
110
111	existingFav := &entity.Favorite{
112		ID:    42,
113		URL:   "https://example.com",
114		Title: "Example Site",
115	}
116
117	// URL is already a favorite
118	favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)
119	favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(nil)
120
121	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
122
123	result, err := uc.Toggle(ctx, "https://example.com", "Example")
124	require.NoError(t, err)
125	require.NotNil(t, result)
126
127	assert.False(t, result.Added)
128	assert.Equal(t, "https://example.com", result.URL)
129	assert.Equal(t, "Example Site", result.Title) // Uses existing title
130	assert.Equal(t, "Favorite removed", result.Message)
131}
132
133func TestManageFavoritesUseCase_Toggle_EmptyURLReturnsError(t *testing.T) {
134	ctx := testContext()
135
136	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
137	folderRepo := repomocks.NewMockFolderRepository(t)
138	tagRepo := repomocks.NewMockTagRepository(t)
139
140	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
141
142	result, err := uc.Toggle(ctx, "", "Title")
143	require.Error(t, err)
144	assert.Nil(t, result)
145	assert.Contains(t, err.Error(), "URL cannot be empty")
146}
147
148func TestManageFavoritesUseCase_Toggle_FindByURLErrorReturnsError(t *testing.T) {
149	ctx := testContext()
150
151	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
152	folderRepo := repomocks.NewMockFolderRepository(t)
153	tagRepo := repomocks.NewMockTagRepository(t)
154
155	favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, errors.New("db error"))
156
157	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
158
159	result, err := uc.Toggle(ctx, "https://example.com", "Example")
160	require.Error(t, err)
161	assert.Nil(t, result)
162	assert.Contains(t, err.Error(), "failed to check existing favorite")
163}
164
165func TestManageFavoritesUseCase_Toggle_SaveErrorReturnsError(t *testing.T) {
166	ctx := testContext()
167
168	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
169	folderRepo := repomocks.NewMockFolderRepository(t)
170	tagRepo := repomocks.NewMockTagRepository(t)
171
172	favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil)
173	favoriteRepo.EXPECT().Save(mock.Anything, mock.Anything).Return(errors.New("db error"))
174
175	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
176
177	result, err := uc.Toggle(ctx, "https://example.com", "Example")
178	require.Error(t, err)
179	assert.Nil(t, result)
180	assert.Contains(t, err.Error(), "failed to add favorite")
181}
182
183func TestManageFavoritesUseCase_Toggle_DeleteErrorReturnsError(t *testing.T) {
184	ctx := testContext()
185
186	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
187	folderRepo := repomocks.NewMockFolderRepository(t)
188	tagRepo := repomocks.NewMockTagRepository(t)
189
190	existingFav := &entity.Favorite{ID: 42, URL: "https://example.com", Title: "Example"}
191	favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)
192	favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(errors.New("db error"))
193
194	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
195
196	result, err := uc.Toggle(ctx, "https://example.com", "Example")
197	require.Error(t, err)
198	assert.Nil(t, result)
199	assert.Contains(t, err.Error(), "failed to remove favorite")
200}
201
202func TestManageFavoritesUseCase_FilterForOmnibox_RanksPrefixBeforeContains(t *testing.T) {
203	ctx := testContext()
204
205	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
206	folderRepo := repomocks.NewMockFolderRepository(t)
207	tagRepo := repomocks.NewMockTagRepository(t)
208
209	favorites := []*entity.Favorite{
210		{ID: 1, URL: "https://example.com/landing", Title: "GitHub docs mirror", Position: 0},
211		{ID: 2, URL: "https://github.com/bnema/dumber", Title: "Project repo", Position: 1},
212		{ID: 3, URL: "https://gitlab.com/team/project", Title: "GitLab", Position: 2},
213	}
214	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
215
216	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
217
218	results, err := uc.FilterForOmnibox(ctx, "git")
219	require.NoError(t, err)
220	require.Len(t, results, 3)
221	assert.Equal(t, "https://github.com/bnema/dumber", results[0].URL)
222	assert.Equal(t, "https://gitlab.com/team/project", results[1].URL)
223	assert.Equal(t, "https://example.com/landing", results[2].URL)
224}
225
226func TestManageFavoritesUseCase_FilterForOmnibox_EmptyQueryKeepsRepositoryOrder(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://first.example"},
235		{ID: 2, URL: "https://second.example"},
236	}
237	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
238
239	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
240
241	results, err := uc.FilterForOmnibox(ctx, "")
242	require.NoError(t, err)
243	require.Len(t, results, 2)
244	assert.Equal(t, "https://first.example", results[0].URL)
245	assert.Equal(t, "https://second.example", results[1].URL)
246}
247
248func TestManageFavoritesUseCase_GetAllURLs_UsesCache(t *testing.T) {
249	ctx := testContext()
250
251	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
252	folderRepo := repomocks.NewMockFolderRepository(t)
253	tagRepo := repomocks.NewMockTagRepository(t)
254
255	favorites := []*entity.Favorite{
256		{ID: 1, URL: "https://example.com"},
257	}
258	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil).Once()
259
260	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
261
262	urls, err := uc.GetAllURLs(ctx)
263	require.NoError(t, err)
264	require.Contains(t, urls, "https://example.com")
265
266	urls, err = uc.GetAllURLs(ctx)
267	require.NoError(t, err)
268	require.Contains(t, urls, "https://example.com")
269}
270
271func TestManageFavoritesUseCase_Toggle_InvalidatesCache(t *testing.T) {
272	ctx := testContext()
273
274	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
275	folderRepo := repomocks.NewMockFolderRepository(t)
276	tagRepo := repomocks.NewMockTagRepository(t)
277
278	initialFavorites := []*entity.Favorite{
279		{ID: 1, URL: "https://cached.example"},
280	}
281	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(initialFavorites, nil).Once()
282	favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://new.example").Return(nil, nil)
283	favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(f *entity.Favorite) bool {
284		return f.URL == "https://new.example"
285	})).Return(nil)
286	updatedFavorites := []*entity.Favorite{
287		{ID: 1, URL: "https://cached.example"},
288		{ID: 2, URL: "https://new.example"},
289	}
290	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(updatedFavorites, nil).Once()
291
292	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
293
294	urls, err := uc.GetAllURLs(ctx)
295	require.NoError(t, err)
296	require.Contains(t, urls, "https://cached.example")
297
298	_, err = uc.Toggle(ctx, "https://new.example", "New")
299	require.NoError(t, err)
300
301	urls, err = uc.GetAllURLs(ctx)
302	require.NoError(t, err)
303	require.Contains(t, urls, "https://new.example")
304}
305
306func TestManageFavoritesUseCase_DeleteFolder_InvalidatesFavoritesCache(t *testing.T) {
307	ctx := testContext()
308
309	favoriteRepo := repomocks.NewMockFavoriteRepository(t)
310	folderRepo := repomocks.NewMockFolderRepository(t)
311	tagRepo := repomocks.NewMockTagRepository(t)
312
313	folderID := entity.FolderID(9)
314	initialFolderID := folderID
315	cachedFavorites := []*entity.Favorite{
316		{ID: 1, URL: "https://example.com", FolderID: &initialFolderID},
317	}
318	updatedFavorites := []*entity.Favorite{
319		{ID: 1, URL: "https://example.com", FolderID: nil},
320	}
321
322	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(cachedFavorites, nil).Once()
323	folderRepo.EXPECT().FindByID(mock.Anything, folderID).Return(&entity.Folder{ID: folderID}, nil).Once()
324	favoriteRepo.EXPECT().GetByFolder(mock.Anything, &folderID).Return(cachedFavorites, nil).Once()
325	favoriteRepo.EXPECT().SetFolder(mock.Anything, entity.FavoriteID(1), (*entity.FolderID)(nil)).Return(nil).Once()
326	folderRepo.EXPECT().GetChildren(mock.Anything, &folderID).Return([]*entity.Folder{}, nil).Once()
327	folderRepo.EXPECT().Delete(mock.Anything, folderID).Return(nil).Once()
328	favoriteRepo.EXPECT().GetAll(mock.Anything).Return(updatedFavorites, nil).Once()
329
330	uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
331
332	favoritesBefore, err := uc.GetAll(ctx)
333	require.NoError(t, err)
334	require.Len(t, favoritesBefore, 1)
335	require.NotNil(t, favoritesBefore[0].FolderID)
336
337	err = uc.DeleteFolder(ctx, folderID)
338	require.NoError(t, err)
339
340	favoritesAfter, err := uc.GetAll(ctx)
341	require.NoError(t, err)
342	require.Len(t, favoritesAfter, 1)
343	assert.Nil(t, favoritesAfter[0].FolderID)
344}

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