detached 45b8a06

refactor(favorites): simplify usecase to tags

commit
45b8a06f5d50e852cf6fc20c79f80958ad914cd8
author
brice · · 2026-06-28 07:32 UTC
parent
8077f20
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +60 -267
M cmd/dumber/main.go +1 -4
@@ -691,7 +691,6 @@ func setupSignalHandler(ctx context.Context, app *ui.App) {
691691 type repositories struct {
692692 history repository.HistoryRepository
693693 favorite repository.FavoriteRepository
694- folder repository.FolderRepository
695694 tag repository.TagRepository
696695 zoom repository.ZoomRepository
697696 permission port.PermissionRepository
@@ -704,7 +703,6 @@ func createRepositories(db *sql.DB) *repositories {
704703 return &repositories{
705704 history: sqlite.NewHistoryRepository(db),
706705 favorite: sqlite.NewFavoriteRepository(db),
707- folder: sqlite.NewFolderRepository(db),
708706 tag: sqlite.NewTagRepository(db),
709707 zoom: sqlite.NewZoomRepository(db),
710708 permission: sqlite.NewPermissionRepository(db),
@@ -718,7 +716,6 @@ func createLazyRepositories(provider port.DatabaseProvider) *repositories {
718716 return &repositories{
719717 history: sqlite.NewLazyHistoryRepository(provider),
720718 favorite: sqlite.NewLazyFavoriteRepository(provider),
721- folder: sqlite.NewLazyFolderRepository(provider),
722719 tag: sqlite.NewLazyTagRepository(provider),
723720 zoom: sqlite.NewLazyZoomRepository(provider),
724721 permission: sqlite.NewLazyPermissionRepository(provider),
@@ -804,7 +801,7 @@ func createUseCases(repos *repositories, cfg *config.Config) *useCases {
804801 tabs: usecase.NewManageTabsUseCase(idGenerator, localPaths),
805802 panes: usecase.NewManagePanesUseCase(idGenerator, localPaths),
806803 history: historyUC,
807- favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.folder, repos.tag),
804+ favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.tag),
808805 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),
809806 permission: permissionUC,
810807 navigate: usecase.NewNavigateUseCase(defaultZoom),
@@ -691,7 +691,6 @@ func setupSignalHandler(ctx context.Context, app *ui.App) {
691 type repositories struct {691 type repositories struct {
692 history repository.HistoryRepository692 history repository.HistoryRepository
693 favorite repository.FavoriteRepository693 favorite repository.FavoriteRepository
694- folder repository.FolderRepository
695 tag repository.TagRepository694 tag repository.TagRepository
696 zoom repository.ZoomRepository695 zoom repository.ZoomRepository
697 permission port.PermissionRepository696 permission port.PermissionRepository
@@ -704,7 +703,6 @@ func createRepositories(db *sql.DB) *repositories {
704 return &repositories{703 return &repositories{
705 history: sqlite.NewHistoryRepository(db),704 history: sqlite.NewHistoryRepository(db),
706 favorite: sqlite.NewFavoriteRepository(db),705 favorite: sqlite.NewFavoriteRepository(db),
707- folder: sqlite.NewFolderRepository(db),
708 tag: sqlite.NewTagRepository(db),706 tag: sqlite.NewTagRepository(db),
709 zoom: sqlite.NewZoomRepository(db),707 zoom: sqlite.NewZoomRepository(db),
710 permission: sqlite.NewPermissionRepository(db),708 permission: sqlite.NewPermissionRepository(db),
@@ -718,7 +716,6 @@ func createLazyRepositories(provider port.DatabaseProvider) *repositories {
718 return &repositories{716 return &repositories{
719 history: sqlite.NewLazyHistoryRepository(provider),717 history: sqlite.NewLazyHistoryRepository(provider),
720 favorite: sqlite.NewLazyFavoriteRepository(provider),718 favorite: sqlite.NewLazyFavoriteRepository(provider),
721- folder: sqlite.NewLazyFolderRepository(provider),
722 tag: sqlite.NewLazyTagRepository(provider),719 tag: sqlite.NewLazyTagRepository(provider),
723 zoom: sqlite.NewLazyZoomRepository(provider),720 zoom: sqlite.NewLazyZoomRepository(provider),
724 permission: sqlite.NewLazyPermissionRepository(provider),721 permission: sqlite.NewLazyPermissionRepository(provider),
@@ -804,7 +801,7 @@ func createUseCases(repos *repositories, cfg *config.Config) *useCases {
804 tabs: usecase.NewManageTabsUseCase(idGenerator, localPaths),801 tabs: usecase.NewManageTabsUseCase(idGenerator, localPaths),
805 panes: usecase.NewManagePanesUseCase(idGenerator, localPaths),802 panes: usecase.NewManagePanesUseCase(idGenerator, localPaths),
806 history: historyUC,803 history: historyUC,
807- favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.folder, repos.tag),804+ favorites: usecase.NewManageFavoritesUseCase(repos.favorite, repos.tag),
808 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),805 zoom: usecase.NewManageZoomUseCase(repos.zoom, defaultZoom, zoomCache),
809 permission: permissionUC,806 permission: permissionUC,
810 navigate: usecase.NewNavigateUseCase(defaultZoom),807 navigate: usecase.NewNavigateUseCase(defaultZoom),
M internal/application/usecase/manage_favorites.go +11 -185
@@ -20,10 +20,9 @@ import (
2020 // Compile-time check: ManageFavoritesUseCase must satisfy port.HomepageFavorites.
2121 var _ port.HomepageFavorites = (*ManageFavoritesUseCase)(nil)
2222
23-// ManageFavoritesUseCase handles favorite, folder, and tag operations.
23+// ManageFavoritesUseCase handles favorite and tag operations.
2424 type ManageFavoritesUseCase struct {
2525 favoriteRepo repository.FavoriteRepository
26- folderRepo repository.FolderRepository
2726 tagRepo repository.TagRepository
2827
2928 cacheMu sync.Mutex
@@ -37,12 +36,10 @@ const favoritesCacheTTL = 2 * time.Second
3736 // NewManageFavoritesUseCase creates a new favorites management use case.
3837 func NewManageFavoritesUseCase(
3938 favoriteRepo repository.FavoriteRepository,
40- folderRepo repository.FolderRepository,
4139 tagRepo repository.TagRepository,
4240 ) *ManageFavoritesUseCase {
4341 return &ManageFavoritesUseCase{
4442 favoriteRepo: favoriteRepo,
45- folderRepo: folderRepo,
4643 tagRepo: tagRepo,
4744 }
4845 }
@@ -52,7 +49,6 @@ type AddFavoriteInput struct {
5249 URL string
5350 Title string
5451 FaviconURL string
55- FolderID *entity.FolderID
5652 Tags []entity.TagID
5753 }
5854
@@ -78,7 +74,6 @@ func (uc *ManageFavoritesUseCase) addNewFavorite(ctx context.Context, input AddF
7874 log := logging.FromContext(ctx)
7975 fav := entity.NewFavorite(input.URL, input.Title)
8076 fav.FaviconURL = input.FaviconURL
81- fav.FolderID = input.FolderID
8277
8378 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {
8479 return nil, fmt.Errorf("failed to save favorite: %w", err)
@@ -125,7 +120,6 @@ func (uc *ManageFavoritesUseCase) AddFavorite(ctx context.Context, input dto.Fav
125120 URL: favoriteURL,
126121 Title: title,
127122 FaviconURL: strings.TrimSpace(input.FaviconURL),
128- FolderID: input.FolderID,
129123 Tags: tags,
130124 })
131125 }
@@ -204,7 +198,6 @@ func (uc *ManageFavoritesUseCase) UpdateFavorite(ctx context.Context, input dto.
204198 }
205199 fav.Title = title
206200 fav.FaviconURL = strings.TrimSpace(input.FaviconURL)
207- fav.FolderID = input.FolderID
208201 fav.ShortcutKey = input.ShortcutKey
209202
210203 if err := uc.Update(ctx, fav); err != nil {
@@ -263,22 +256,6 @@ func (uc *ManageFavoritesUseCase) Update(ctx context.Context, fav *entity.Favori
263256 return nil
264257 }
265258
266-// Move changes a favorite's folder.
267-func (uc *ManageFavoritesUseCase) Move(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
268- log := logging.FromContext(ctx)
269- log.Debug().
270- Int64("favorite_id", int64(id)).
271- Msg("moving favorite to folder")
272-
273- if err := uc.favoriteRepo.SetFolder(ctx, id, folderID); err != nil {
274- return fmt.Errorf("failed to move favorite: %w", err)
275- }
276-
277- log.Info().Int64("id", int64(id)).Msg("favorite moved")
278- uc.invalidateCache()
279- return nil
280-}
281-
282259 // SetShortcut assigns or removes a keyboard shortcut (1-9).
283260 func (uc *ManageFavoritesUseCase) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
284261 log := logging.FromContext(ctx)
@@ -536,141 +513,13 @@ func (uc *ManageFavoritesUseCase) invalidateCache() {
536513 uc.cacheTime = time.Time{}
537514 }
538515
539-// GetTree builds a complete hierarchical view of folders and favorites.
540-func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) {
541- log := logging.FromContext(ctx)
542- log.Debug().Msg("building favorite tree")
543-
544- tree := entity.NewFavoriteTree()
545-
546- // Get all folders
547- folders, err := uc.folderRepo.GetAll(ctx)
548- if err != nil {
549- return nil, fmt.Errorf("failed to get folders: %w", err)
550- }
551-
552- // Build folder map and organize by parent
553- for _, folder := range folders {
554- tree.FolderMap[folder.ID] = folder
555- if folder.ParentID == nil {
556- tree.RootFolders = append(tree.RootFolders, folder)
557- } else {
558- tree.ChildFolders[*folder.ParentID] = append(tree.ChildFolders[*folder.ParentID], folder)
559- }
560- }
561-
562- // Get all favorites
563- favorites, err := uc.favoriteRepo.GetAll(ctx)
564- if err != nil {
565- return nil, fmt.Errorf("failed to get favorites: %w", err)
566- }
567-
568- // Organize favorites by folder
569- for _, fav := range favorites {
570- if fav.FolderID == nil {
571- tree.RootFavorites = append(tree.RootFavorites, fav)
572- } else {
573- tree.ChildFavorites[*fav.FolderID] = append(tree.ChildFavorites[*fav.FolderID], fav)
574- }
575- }
576-
577- log.Debug().
578- Int("folders", len(folders)).
579- Int("favorites", len(favorites)).
580- Msg("favorite tree built")
581-
582- return tree, nil
583-}
584-
585-// CreateFolder creates a new folder.
586-func (uc *ManageFavoritesUseCase) CreateFolder(ctx context.Context, name, icon string, parentID *entity.FolderID) (*entity.Folder, error) {
516+// AddTag creates a new tag.
517+func (uc *ManageFavoritesUseCase) AddTag(ctx context.Context, name, color string) (*entity.Tag, error) {
587518 name = strings.TrimSpace(name)
588- icon = strings.TrimSpace(icon)
519+ color = strings.TrimSpace(color)
589520 if name == "" {
590- return nil, fmt.Errorf("folder name is required")
591- }
592- log := logging.FromContext(ctx)
593- log.Debug().Str("name", name).Msg("creating folder")
594-
595- folder := entity.NewFolder(name)
596- folder.Icon = icon
597- folder.ParentID = parentID
598-
599- if err := uc.folderRepo.Save(ctx, folder); err != nil {
600- return nil, fmt.Errorf("failed to create folder: %w", err)
601- }
602-
603- log.Info().Str("name", name).Int64("id", int64(folder.ID)).Msg("folder created")
604- return folder, nil
605-}
606-
607-// DeleteFolder removes a folder. Favorites in the folder are moved to root.
608-func (uc *ManageFavoritesUseCase) DeleteFolder(ctx context.Context, id entity.FolderID) error {
609- log := logging.FromContext(ctx)
610- log.Debug().Int64("id", int64(id)).Msg("deleting folder")
611-
612- // Move favorites from this folder to root (or parent)
613- folder, err := uc.folderRepo.FindByID(ctx, id)
614- if err != nil {
615- return fmt.Errorf("failed to find folder: %w", err)
616- }
617- if folder == nil {
618- log.Debug().Int64("id", int64(id)).Msg("folder not found")
619- return nil
620- }
621-
622- // Get favorites in this folder
623- favorites, err := uc.favoriteRepo.GetByFolder(ctx, &id)
624- if err != nil {
625- return fmt.Errorf("failed to get folder favorites: %w", err)
626- }
627-
628- // Move favorites to parent folder (or root)
629- for _, fav := range favorites {
630- if setErr := uc.favoriteRepo.SetFolder(ctx, fav.ID, folder.ParentID); setErr != nil {
631- log.Warn().
632- Int64("favorite_id", int64(fav.ID)).
633- Err(setErr).
634- Msg("failed to move favorite from deleted folder")
635- }
636- }
637- uc.invalidateCache()
638-
639- // Get child folders
640- children, err := uc.folderRepo.GetChildren(ctx, &id)
641- if err != nil {
642- return fmt.Errorf("failed to get child folders: %w", err)
521+ return nil, fmt.Errorf("tag name is required")
643522 }
644-
645- // Recursively delete child folders
646- for _, child := range children {
647- if err := uc.DeleteFolder(ctx, child.ID); err != nil {
648- log.Warn().
649- Int64("child_id", int64(child.ID)).
650- Err(err).
651- Msg("failed to delete child folder")
652- }
653- }
654-
655- // Delete the folder itself
656- if err := uc.folderRepo.Delete(ctx, id); err != nil {
657- return fmt.Errorf("failed to delete folder: %w", err)
658- }
659-
660- log.Info().Int64("id", int64(id)).Msg("folder deleted")
661- return nil
662-}
663-
664-// GetAllFolders retrieves all folders.
665-func (uc *ManageFavoritesUseCase) GetAllFolders(ctx context.Context) ([]*entity.Folder, error) {
666- log := logging.FromContext(ctx)
667- log.Debug().Msg("getting all folders")
668-
669- return uc.folderRepo.GetAll(ctx)
670-}
671-
672-// AddTag creates a new tag.
673-func (uc *ManageFavoritesUseCase) AddTag(ctx context.Context, name, color string) (*entity.Tag, error) {
674523 log := logging.FromContext(ctx)
675524 log.Debug().Str("name", name).Str("color", color).Msg("creating tag")
676525
@@ -759,34 +608,13 @@ func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID
759608 return uc.tagRepo.GetForFavorite(ctx, favID)
760609 }
761610
762-// UpdateFolder updates a folder's name and icon.
763-func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error {
764- log := logging.FromContext(ctx)
765- log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder")
766-
767- folder, err := uc.folderRepo.FindByID(ctx, id)
768- if err != nil {
769- return fmt.Errorf("failed to find folder: %w", err)
770- }
771- if folder == nil {
772- return fmt.Errorf("folder %d not found", id)
773- }
774-
775- if name != "" {
776- folder.Name = name
777- }
778- folder.Icon = icon
779-
780- if err := uc.folderRepo.Save(ctx, folder); err != nil {
781- return fmt.Errorf("failed to update folder: %w", err)
782- }
783-
784- log.Info().Int64("id", int64(id)).Msg("folder updated")
785- return nil
786-}
787-
788611 // UpdateTag updates a tag's name and color.
789612 func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error {
613+ name = strings.TrimSpace(name)
614+ color = strings.TrimSpace(color)
615+ if name == "" {
616+ return fmt.Errorf("tag name is required")
617+ }
790618 log := logging.FromContext(ctx)
791619 log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag")
792620
@@ -798,9 +626,7 @@ func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID
798626 return fmt.Errorf("tag %d not found", id)
799627 }
800628
801- if name != "" {
802- tag.Name = name
803- }
629+ tag.Name = name
804630 if color != "" {
805631 tag.Color = color
806632 }
@@ -20,10 +20,9 @@ import (
20 // Compile-time check: ManageFavoritesUseCase must satisfy port.HomepageFavorites.20 // Compile-time check: ManageFavoritesUseCase must satisfy port.HomepageFavorites.
21 var _ port.HomepageFavorites = (*ManageFavoritesUseCase)(nil)21 var _ port.HomepageFavorites = (*ManageFavoritesUseCase)(nil)
22 22
23-// ManageFavoritesUseCase handles favorite, folder, and tag operations.23+// ManageFavoritesUseCase handles favorite and tag operations.
24 type ManageFavoritesUseCase struct {24 type ManageFavoritesUseCase struct {
25 favoriteRepo repository.FavoriteRepository25 favoriteRepo repository.FavoriteRepository
26- folderRepo repository.FolderRepository
27 tagRepo repository.TagRepository26 tagRepo repository.TagRepository
28 27
29 cacheMu sync.Mutex28 cacheMu sync.Mutex
@@ -37,12 +36,10 @@ const favoritesCacheTTL = 2 * time.Second
37 // NewManageFavoritesUseCase creates a new favorites management use case.36 // NewManageFavoritesUseCase creates a new favorites management use case.
38 func NewManageFavoritesUseCase(37 func NewManageFavoritesUseCase(
39 favoriteRepo repository.FavoriteRepository,38 favoriteRepo repository.FavoriteRepository,
40- folderRepo repository.FolderRepository,
41 tagRepo repository.TagRepository,39 tagRepo repository.TagRepository,
42 ) *ManageFavoritesUseCase {40 ) *ManageFavoritesUseCase {
43 return &ManageFavoritesUseCase{41 return &ManageFavoritesUseCase{
44 favoriteRepo: favoriteRepo,42 favoriteRepo: favoriteRepo,
45- folderRepo: folderRepo,
46 tagRepo: tagRepo,43 tagRepo: tagRepo,
47 }44 }
48 }45 }
@@ -52,7 +49,6 @@ type AddFavoriteInput struct {
52 URL string49 URL string
53 Title string50 Title string
54 FaviconURL string51 FaviconURL string
55- FolderID *entity.FolderID
56 Tags []entity.TagID52 Tags []entity.TagID
57 }53 }
58 54
@@ -78,7 +74,6 @@ func (uc *ManageFavoritesUseCase) addNewFavorite(ctx context.Context, input AddF
78 log := logging.FromContext(ctx)74 log := logging.FromContext(ctx)
79 fav := entity.NewFavorite(input.URL, input.Title)75 fav := entity.NewFavorite(input.URL, input.Title)
80 fav.FaviconURL = input.FaviconURL76 fav.FaviconURL = input.FaviconURL
81- fav.FolderID = input.FolderID
82 77
83 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {78 if err := uc.favoriteRepo.Save(ctx, fav); err != nil {
84 return nil, fmt.Errorf("failed to save favorite: %w", err)79 return nil, fmt.Errorf("failed to save favorite: %w", err)
@@ -125,7 +120,6 @@ func (uc *ManageFavoritesUseCase) AddFavorite(ctx context.Context, input dto.Fav
125 URL: favoriteURL,120 URL: favoriteURL,
126 Title: title,121 Title: title,
127 FaviconURL: strings.TrimSpace(input.FaviconURL),122 FaviconURL: strings.TrimSpace(input.FaviconURL),
128- FolderID: input.FolderID,
129 Tags: tags,123 Tags: tags,
130 })124 })
131 }125 }
@@ -204,7 +198,6 @@ func (uc *ManageFavoritesUseCase) UpdateFavorite(ctx context.Context, input dto.
204 }198 }
205 fav.Title = title199 fav.Title = title
206 fav.FaviconURL = strings.TrimSpace(input.FaviconURL)200 fav.FaviconURL = strings.TrimSpace(input.FaviconURL)
207- fav.FolderID = input.FolderID
208 fav.ShortcutKey = input.ShortcutKey201 fav.ShortcutKey = input.ShortcutKey
209 202
210 if err := uc.Update(ctx, fav); err != nil {203 if err := uc.Update(ctx, fav); err != nil {
@@ -263,22 +256,6 @@ func (uc *ManageFavoritesUseCase) Update(ctx context.Context, fav *entity.Favori
263 return nil256 return nil
264 }257 }
265 258
266-// Move changes a favorite's folder.
267-func (uc *ManageFavoritesUseCase) Move(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
268- log := logging.FromContext(ctx)
269- log.Debug().
270- Int64("favorite_id", int64(id)).
271- Msg("moving favorite to folder")
272-
273- if err := uc.favoriteRepo.SetFolder(ctx, id, folderID); err != nil {
274- return fmt.Errorf("failed to move favorite: %w", err)
275- }
276-
277- log.Info().Int64("id", int64(id)).Msg("favorite moved")
278- uc.invalidateCache()
279- return nil
280-}
281-
282 // SetShortcut assigns or removes a keyboard shortcut (1-9).259 // SetShortcut assigns or removes a keyboard shortcut (1-9).
283 func (uc *ManageFavoritesUseCase) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {260 func (uc *ManageFavoritesUseCase) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
284 log := logging.FromContext(ctx)261 log := logging.FromContext(ctx)
@@ -536,141 +513,13 @@ func (uc *ManageFavoritesUseCase) invalidateCache() {
536 uc.cacheTime = time.Time{}513 uc.cacheTime = time.Time{}
537 }514 }
538 515
539-// GetTree builds a complete hierarchical view of folders and favorites.516+// AddTag creates a new tag.
540-func (uc *ManageFavoritesUseCase) GetTree(ctx context.Context) (*entity.FavoriteTree, error) {517+func (uc *ManageFavoritesUseCase) AddTag(ctx context.Context, name, color string) (*entity.Tag, error) {
541- log := logging.FromContext(ctx)
542- log.Debug().Msg("building favorite tree")
543-
544- tree := entity.NewFavoriteTree()
545-
546- // Get all folders
547- folders, err := uc.folderRepo.GetAll(ctx)
548- if err != nil {
549- return nil, fmt.Errorf("failed to get folders: %w", err)
550- }
551-
552- // Build folder map and organize by parent
553- for _, folder := range folders {
554- tree.FolderMap[folder.ID] = folder
555- if folder.ParentID == nil {
556- tree.RootFolders = append(tree.RootFolders, folder)
557- } else {
558- tree.ChildFolders[*folder.ParentID] = append(tree.ChildFolders[*folder.ParentID], folder)
559- }
560- }
561-
562- // Get all favorites
563- favorites, err := uc.favoriteRepo.GetAll(ctx)
564- if err != nil {
565- return nil, fmt.Errorf("failed to get favorites: %w", err)
566- }
567-
568- // Organize favorites by folder
569- for _, fav := range favorites {
570- if fav.FolderID == nil {
571- tree.RootFavorites = append(tree.RootFavorites, fav)
572- } else {
573- tree.ChildFavorites[*fav.FolderID] = append(tree.ChildFavorites[*fav.FolderID], fav)
574- }
575- }
576-
577- log.Debug().
578- Int("folders", len(folders)).
579- Int("favorites", len(favorites)).
580- Msg("favorite tree built")
581-
582- return tree, nil
583-}
584-
585-// CreateFolder creates a new folder.
586-func (uc *ManageFavoritesUseCase) CreateFolder(ctx context.Context, name, icon string, parentID *entity.FolderID) (*entity.Folder, error) {
587 name = strings.TrimSpace(name)518 name = strings.TrimSpace(name)
588- icon = strings.TrimSpace(icon)519+ color = strings.TrimSpace(color)
589 if name == "" {520 if name == "" {
590- return nil, fmt.Errorf("folder name is required")521+ return nil, fmt.Errorf("tag name is required")
591- }
592- log := logging.FromContext(ctx)
593- log.Debug().Str("name", name).Msg("creating folder")
594-
595- folder := entity.NewFolder(name)
596- folder.Icon = icon
597- folder.ParentID = parentID
598-
599- if err := uc.folderRepo.Save(ctx, folder); err != nil {
600- return nil, fmt.Errorf("failed to create folder: %w", err)
601- }
602-
603- log.Info().Str("name", name).Int64("id", int64(folder.ID)).Msg("folder created")
604- return folder, nil
605-}
606-
607-// DeleteFolder removes a folder. Favorites in the folder are moved to root.
608-func (uc *ManageFavoritesUseCase) DeleteFolder(ctx context.Context, id entity.FolderID) error {
609- log := logging.FromContext(ctx)
610- log.Debug().Int64("id", int64(id)).Msg("deleting folder")
611-
612- // Move favorites from this folder to root (or parent)
613- folder, err := uc.folderRepo.FindByID(ctx, id)
614- if err != nil {
615- return fmt.Errorf("failed to find folder: %w", err)
616- }
617- if folder == nil {
618- log.Debug().Int64("id", int64(id)).Msg("folder not found")
619- return nil
620- }
621-
622- // Get favorites in this folder
623- favorites, err := uc.favoriteRepo.GetByFolder(ctx, &id)
624- if err != nil {
625- return fmt.Errorf("failed to get folder favorites: %w", err)
626- }
627-
628- // Move favorites to parent folder (or root)
629- for _, fav := range favorites {
630- if setErr := uc.favoriteRepo.SetFolder(ctx, fav.ID, folder.ParentID); setErr != nil {
631- log.Warn().
632- Int64("favorite_id", int64(fav.ID)).
633- Err(setErr).
634- Msg("failed to move favorite from deleted folder")
635- }
636- }
637- uc.invalidateCache()
638-
639- // Get child folders
640- children, err := uc.folderRepo.GetChildren(ctx, &id)
641- if err != nil {
642- return fmt.Errorf("failed to get child folders: %w", err)
643 }522 }
644-
645- // Recursively delete child folders
646- for _, child := range children {
647- if err := uc.DeleteFolder(ctx, child.ID); err != nil {
648- log.Warn().
649- Int64("child_id", int64(child.ID)).
650- Err(err).
651- Msg("failed to delete child folder")
652- }
653- }
654-
655- // Delete the folder itself
656- if err := uc.folderRepo.Delete(ctx, id); err != nil {
657- return fmt.Errorf("failed to delete folder: %w", err)
658- }
659-
660- log.Info().Int64("id", int64(id)).Msg("folder deleted")
661- return nil
662-}
663-
664-// GetAllFolders retrieves all folders.
665-func (uc *ManageFavoritesUseCase) GetAllFolders(ctx context.Context) ([]*entity.Folder, error) {
666- log := logging.FromContext(ctx)
667- log.Debug().Msg("getting all folders")
668-
669- return uc.folderRepo.GetAll(ctx)
670-}
671-
672-// AddTag creates a new tag.
673-func (uc *ManageFavoritesUseCase) AddTag(ctx context.Context, name, color string) (*entity.Tag, error) {
674 log := logging.FromContext(ctx)523 log := logging.FromContext(ctx)
675 log.Debug().Str("name", name).Str("color", color).Msg("creating tag")524 log.Debug().Str("name", name).Str("color", color).Msg("creating tag")
676 525
@@ -759,34 +608,13 @@ func (uc *ManageFavoritesUseCase) GetTagsForFavorite(ctx context.Context, favID
759 return uc.tagRepo.GetForFavorite(ctx, favID)608 return uc.tagRepo.GetForFavorite(ctx, favID)
760 }609 }
761 610
762-// UpdateFolder updates a folder's name and icon.
763-func (uc *ManageFavoritesUseCase) UpdateFolder(ctx context.Context, id entity.FolderID, name, icon string) error {
764- log := logging.FromContext(ctx)
765- log.Debug().Int64("id", int64(id)).Str("name", name).Msg("updating folder")
766-
767- folder, err := uc.folderRepo.FindByID(ctx, id)
768- if err != nil {
769- return fmt.Errorf("failed to find folder: %w", err)
770- }
771- if folder == nil {
772- return fmt.Errorf("folder %d not found", id)
773- }
774-
775- if name != "" {
776- folder.Name = name
777- }
778- folder.Icon = icon
779-
780- if err := uc.folderRepo.Save(ctx, folder); err != nil {
781- return fmt.Errorf("failed to update folder: %w", err)
782- }
783-
784- log.Info().Int64("id", int64(id)).Msg("folder updated")
785- return nil
786-}
787-
788 // UpdateTag updates a tag's name and color.611 // UpdateTag updates a tag's name and color.
789 func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error {612 func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID, name, color string) error {
613+ name = strings.TrimSpace(name)
614+ color = strings.TrimSpace(color)
615+ if name == "" {
616+ return fmt.Errorf("tag name is required")
617+ }
790 log := logging.FromContext(ctx)618 log := logging.FromContext(ctx)
791 log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag")619 log.Debug().Int64("id", int64(id)).Str("name", name).Str("color", color).Msg("updating tag")
792 620
@@ -798,9 +626,7 @@ func (uc *ManageFavoritesUseCase) UpdateTag(ctx context.Context, id entity.TagID
798 return fmt.Errorf("tag %d not found", id)626 return fmt.Errorf("tag %d not found", id)
799 }627 }
800 628
801- if name != "" {629+ tag.Name = name
802- tag.Name = name
803- }
804 if color != "" {630 if color != "" {
805 tag.Color = color631 tag.Color = color
806 }632 }
M internal/application/usecase/manage_favorites_cache_test.go +1 -2
@@ -17,7 +17,6 @@ func TestManageFavoritesUseCase_GetAllDoesNotOverwriteFreshCache(t *testing.T) {
1717 ctx := logging.WithContext(context.Background(), logger)
1818
1919 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
20- folderRepo := repomocks.NewMockFolderRepository(t)
2120 tagRepo := repomocks.NewMockTagRepository(t)
2221
2322 fetchedFavorites := []*entity.Favorite{
@@ -27,7 +26,7 @@ func TestManageFavoritesUseCase_GetAllDoesNotOverwriteFreshCache(t *testing.T) {
2726 {ID: 2, URL: "https://fresh.example"},
2827 }
2928
30- uc := NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
29+ uc := NewManageFavoritesUseCase(favoriteRepo, tagRepo)
3130 favoriteRepo.EXPECT().GetAll(ctx).Run(func(_ context.Context) {
3231 uc.cacheMu.Lock()
3332 uc.favoritesCache = refreshedFavorites
@@ -17,7 +17,6 @@ func TestManageFavoritesUseCase_GetAllDoesNotOverwriteFreshCache(t *testing.T) {
17 ctx := logging.WithContext(context.Background(), logger)17 ctx := logging.WithContext(context.Background(), logger)
18 18
19 favoriteRepo := repomocks.NewMockFavoriteRepository(t)19 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
20- folderRepo := repomocks.NewMockFolderRepository(t)
21 tagRepo := repomocks.NewMockTagRepository(t)20 tagRepo := repomocks.NewMockTagRepository(t)
22 21
23 fetchedFavorites := []*entity.Favorite{22 fetchedFavorites := []*entity.Favorite{
@@ -27,7 +26,7 @@ func TestManageFavoritesUseCase_GetAllDoesNotOverwriteFreshCache(t *testing.T) {
27 {ID: 2, URL: "https://fresh.example"},26 {ID: 2, URL: "https://fresh.example"},
28 }27 }
29 28
30- uc := NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)29+ uc := NewManageFavoritesUseCase(favoriteRepo, tagRepo)
31 favoriteRepo.EXPECT().GetAll(ctx).Run(func(_ context.Context) {30 favoriteRepo.EXPECT().GetAll(ctx).Run(func(_ context.Context) {
32 uc.cacheMu.Lock()31 uc.cacheMu.Lock()
33 uc.favoritesCache = refreshedFavorites32 uc.favoritesCache = refreshedFavorites
M internal/application/usecase/manage_favorites_test.go +47 -76
@@ -1,6 +1,7 @@
11 package usecase_test
22
33 import (
4+ "context"
45 "errors"
56 "testing"
67
@@ -17,7 +18,6 @@ func TestManageFavoritesUseCase_GetAllURLs_ReturnsURLSet(t *testing.T) {
1718 ctx := testContext()
1819
1920 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
20- folderRepo := repomocks.NewMockFolderRepository(t)
2121 tagRepo := repomocks.NewMockTagRepository(t)
2222
2323 favorites := []*entity.Favorite{
@@ -28,7 +28,7 @@ func TestManageFavoritesUseCase_GetAllURLs_ReturnsURLSet(t *testing.T) {
2828
2929 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
3030
31- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
31+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
3232
3333 urls, err := uc.GetAllURLs(ctx)
3434 require.NoError(t, err)
@@ -47,12 +47,11 @@ func TestManageFavoritesUseCase_GetAllURLs_ReturnsEmptySetWhenNoFavorites(t *tes
4747 ctx := testContext()
4848
4949 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
50- folderRepo := repomocks.NewMockFolderRepository(t)
5150 tagRepo := repomocks.NewMockTagRepository(t)
5251
5352 favoriteRepo.EXPECT().GetAll(mock.Anything).Return([]*entity.Favorite{}, nil)
5453
55- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
54+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
5655
5756 urls, err := uc.GetAllURLs(ctx)
5857 require.NoError(t, err)
@@ -64,12 +63,11 @@ func TestManageFavoritesUseCase_GetAllURLs_ReturnsErrorOnRepoFailure(t *testing.
6463 ctx := testContext()
6564
6665 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
67- folderRepo := repomocks.NewMockFolderRepository(t)
6866 tagRepo := repomocks.NewMockTagRepository(t)
6967
7068 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(nil, errors.New("db connection failed"))
7169
72- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
70+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
7371
7472 urls, err := uc.GetAllURLs(ctx)
7573 require.Error(t, err)
@@ -81,7 +79,6 @@ func TestManageFavoritesUseCase_Toggle_AddsWhenNotFavorite(t *testing.T) {
8179 ctx := testContext()
8280
8381 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
84- folderRepo := repomocks.NewMockFolderRepository(t)
8582 tagRepo := repomocks.NewMockTagRepository(t)
8683
8784 // URL is not a favorite
@@ -90,7 +87,7 @@ func TestManageFavoritesUseCase_Toggle_AddsWhenNotFavorite(t *testing.T) {
9087 return f.URL == "https://example.com" && f.Title == "Example"
9188 })).Return(nil)
9289
93- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
90+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
9491
9592 result, err := uc.Toggle(ctx, "https://example.com", "Example")
9693 require.NoError(t, err)
@@ -106,7 +103,6 @@ func TestManageFavoritesUseCase_Toggle_RemovesWhenAlreadyFavorite(t *testing.T)
106103 ctx := testContext()
107104
108105 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
109- folderRepo := repomocks.NewMockFolderRepository(t)
110106 tagRepo := repomocks.NewMockTagRepository(t)
111107
112108 existingFav := &entity.Favorite{
@@ -119,7 +115,7 @@ func TestManageFavoritesUseCase_Toggle_RemovesWhenAlreadyFavorite(t *testing.T)
119115 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)
120116 favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(nil)
121117
122- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
118+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
123119
124120 result, err := uc.Toggle(ctx, "https://example.com", "Example")
125121 require.NoError(t, err)
@@ -135,10 +131,9 @@ func TestManageFavoritesUseCase_Toggle_EmptyURLReturnsError(t *testing.T) {
135131 ctx := testContext()
136132
137133 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
138- folderRepo := repomocks.NewMockFolderRepository(t)
139134 tagRepo := repomocks.NewMockTagRepository(t)
140135
141- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
136+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
142137
143138 result, err := uc.Toggle(ctx, "", "Title")
144139 require.Error(t, err)
@@ -150,7 +145,6 @@ func TestManageFavoritesUseCase_ToggleNormalizesURLBeforeLookup(t *testing.T) {
150145 ctx := testContext()
151146
152147 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
153- folderRepo := repomocks.NewMockFolderRepository(t)
154148 tagRepo := repomocks.NewMockTagRepository(t)
155149
156150 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()
@@ -159,7 +153,7 @@ func TestManageFavoritesUseCase_ToggleNormalizesURLBeforeLookup(t *testing.T) {
159153 return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example"
160154 })).Return(nil).Once()
161155
162- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
156+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
163157
164158 result, err := uc.Toggle(ctx, " example.com ", " Example ")
165159 require.NoError(t, err)
@@ -173,12 +167,11 @@ func TestManageFavoritesUseCase_Toggle_FindByURLErrorReturnsError(t *testing.T)
173167 ctx := testContext()
174168
175169 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
176- folderRepo := repomocks.NewMockFolderRepository(t)
177170 tagRepo := repomocks.NewMockTagRepository(t)
178171
179172 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, errors.New("db error"))
180173
181- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
174+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
182175
183176 result, err := uc.Toggle(ctx, "https://example.com", "Example")
184177 require.Error(t, err)
@@ -190,13 +183,12 @@ func TestManageFavoritesUseCase_Toggle_SaveErrorReturnsError(t *testing.T) {
190183 ctx := testContext()
191184
192185 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
193- folderRepo := repomocks.NewMockFolderRepository(t)
194186 tagRepo := repomocks.NewMockTagRepository(t)
195187
196188 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil)
197189 favoriteRepo.EXPECT().Save(mock.Anything, mock.Anything).Return(errors.New("db error"))
198190
199- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
191+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
200192
201193 result, err := uc.Toggle(ctx, "https://example.com", "Example")
202194 require.Error(t, err)
@@ -208,14 +200,13 @@ func TestManageFavoritesUseCase_Toggle_DeleteErrorReturnsError(t *testing.T) {
208200 ctx := testContext()
209201
210202 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
211- folderRepo := repomocks.NewMockFolderRepository(t)
212203 tagRepo := repomocks.NewMockTagRepository(t)
213204
214205 existingFav := &entity.Favorite{ID: 42, URL: "https://example.com", Title: "Example"}
215206 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)
216207 favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(errors.New("db error"))
217208
218- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
209+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
219210
220211 result, err := uc.Toggle(ctx, "https://example.com", "Example")
221212 require.Error(t, err)
@@ -227,7 +218,6 @@ func TestManageFavoritesUseCase_FilterForOmnibox_RanksPrefixBeforeContains(t *te
227218 ctx := testContext()
228219
229220 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
230- folderRepo := repomocks.NewMockFolderRepository(t)
231221 tagRepo := repomocks.NewMockTagRepository(t)
232222
233223 favorites := []*entity.Favorite{
@@ -237,7 +227,7 @@ func TestManageFavoritesUseCase_FilterForOmnibox_RanksPrefixBeforeContains(t *te
237227 }
238228 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
239229
240- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
230+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
241231
242232 results, err := uc.FilterForOmnibox(ctx, "git")
243233 require.NoError(t, err)
@@ -251,7 +241,6 @@ func TestManageFavoritesUseCase_FilterForOmnibox_EmptyQueryKeepsRepositoryOrder(
251241 ctx := testContext()
252242
253243 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
254- folderRepo := repomocks.NewMockFolderRepository(t)
255244 tagRepo := repomocks.NewMockTagRepository(t)
256245
257246 favorites := []*entity.Favorite{
@@ -260,7 +249,7 @@ func TestManageFavoritesUseCase_FilterForOmnibox_EmptyQueryKeepsRepositoryOrder(
260249 }
261250 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
262251
263- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
252+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
264253
265254 results, err := uc.FilterForOmnibox(ctx, "")
266255 require.NoError(t, err)
@@ -273,7 +262,6 @@ func TestManageFavoritesUseCase_GetAllURLs_UsesCache(t *testing.T) {
273262 ctx := testContext()
274263
275264 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
276- folderRepo := repomocks.NewMockFolderRepository(t)
277265 tagRepo := repomocks.NewMockTagRepository(t)
278266
279267 favorites := []*entity.Favorite{
@@ -281,7 +269,7 @@ func TestManageFavoritesUseCase_GetAllURLs_UsesCache(t *testing.T) {
281269 }
282270 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil).Once()
283271
284- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
272+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
285273
286274 urls, err := uc.GetAllURLs(ctx)
287275 require.NoError(t, err)
@@ -296,7 +284,6 @@ func TestManageFavoritesUseCase_Toggle_InvalidatesCache(t *testing.T) {
296284 ctx := testContext()
297285
298286 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
299- folderRepo := repomocks.NewMockFolderRepository(t)
300287 tagRepo := repomocks.NewMockTagRepository(t)
301288
302289 initialFavorites := []*entity.Favorite{
@@ -313,7 +300,7 @@ func TestManageFavoritesUseCase_Toggle_InvalidatesCache(t *testing.T) {
313300 }
314301 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(updatedFavorites, nil).Once()
315302
316- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
303+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
317304
318305 urls, err := uc.GetAllURLs(ctx)
319306 require.NoError(t, err)
@@ -327,51 +314,10 @@ func TestManageFavoritesUseCase_Toggle_InvalidatesCache(t *testing.T) {
327314 require.Contains(t, urls, "https://new.example")
328315 }
329316
330-func 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-
370317 func TestManageFavoritesUseCase_AddFavoriteNormalizesURL(t *testing.T) {
371318 ctx := testContext()
372319
373320 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
374- folderRepo := repomocks.NewMockFolderRepository(t)
375321 tagRepo := repomocks.NewMockTagRepository(t)
376322
377323 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()
@@ -380,7 +326,7 @@ func TestManageFavoritesUseCase_AddFavoriteNormalizesURL(t *testing.T) {
380326 return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example"
381327 })).Return(nil).Once()
382328
383- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
329+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
384330
385331 fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "example.com", Title: " Example "})
386332 require.NoError(t, err)
@@ -393,15 +339,42 @@ func TestManageFavoritesUseCase_AddFavoriteRejectsInvalidTagIDs(t *testing.T) {
393339 ctx := testContext()
394340
395341 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
396- folderRepo := repomocks.NewMockFolderRepository(t)
397342 tagRepo := repomocks.NewMockTagRepository(t)
398- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
343+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
399344
400345 _, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "https://example.com", Tags: []entity.TagID{1, 0}})
401346 require.Error(t, err)
402347 assert.Contains(t, err.Error(), "favorite tag id must be positive")
403348 }
404349
350+func TestManageFavoritesUseCase_AddFavoriteAssignsTagsWithoutFolder(t *testing.T) {
351+ ctx := testContext()
352+
353+ favoriteRepo := repomocks.NewMockFavoriteRepository(t)
354+ tagRepo := repomocks.NewMockTagRepository(t)
355+
356+ favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()
357+ favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(fav *entity.Favorite) bool {
358+ return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example" && fav.FaviconURL == "https://example.com/favicon.ico"
359+ })).Run(func(_ context.Context, fav *entity.Favorite) {
360+ fav.ID = entity.FavoriteID(42)
361+ }).Return(nil).Once()
362+ tagRepo.EXPECT().AssignToFavorite(mock.Anything, entity.TagID(7), entity.FavoriteID(42)).Return(nil).Once()
363+ tagRepo.EXPECT().AssignToFavorite(mock.Anything, entity.TagID(8), entity.FavoriteID(42)).Return(nil).Once()
364+
365+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
366+
367+ fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{
368+ URL: "https://example.com",
369+ Title: "Example",
370+ FaviconURL: " https://example.com/favicon.ico ",
371+ Tags: []entity.TagID{7, 8},
372+ })
373+ require.NoError(t, err)
374+ require.NotNil(t, fav)
375+ assert.Equal(t, entity.FavoriteID(42), fav.ID)
376+}
377+
405378 func TestManageFavoritesUseCase_AddFavoriteRejectsUnsafeOrMalformedURL(t *testing.T) {
406379 ctx := testContext()
407380
@@ -416,9 +389,8 @@ func TestManageFavoritesUseCase_AddFavoriteRejectsUnsafeOrMalformedURL(t *testin
416389 for _, raw := range tests {
417390 t.Run(raw, func(t *testing.T) {
418391 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
419- folderRepo := repomocks.NewMockFolderRepository(t)
420392 tagRepo := repomocks.NewMockTagRepository(t)
421- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
393+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
422394
423395 _, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: raw})
424396 require.Error(t, err)
@@ -430,7 +402,6 @@ func TestManageFavoritesUseCase_AddFavoriteNormalizesOpaqueDumbRoute(t *testing.
430402 ctx := testContext()
431403
432404 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
433- folderRepo := repomocks.NewMockFolderRepository(t)
434405 tagRepo := repomocks.NewMockTagRepository(t)
435406
436407 favoriteRepo.EXPECT().FindByURL(mock.Anything, "dumb://history").Return(nil, nil).Once()
@@ -439,7 +410,7 @@ func TestManageFavoritesUseCase_AddFavoriteNormalizesOpaqueDumbRoute(t *testing.
439410 return fav != nil && fav.URL == "dumb://history" && fav.Title == "History"
440411 })).Return(nil).Once()
441412
442- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
413+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
443414
444415 fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "dumb:history", Title: "History"})
445416 require.NoError(t, err)
@@ -1,6 +1,7 @@
1 package usecase_test1 package usecase_test
2 2
3 import (3 import (
4+ "context"
4 "errors"5 "errors"
5 "testing"6 "testing"
6 7
@@ -17,7 +18,6 @@ func TestManageFavoritesUseCase_GetAllURLs_ReturnsURLSet(t *testing.T) {
17 ctx := testContext()18 ctx := testContext()
18 19
19 favoriteRepo := repomocks.NewMockFavoriteRepository(t)20 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
20- folderRepo := repomocks.NewMockFolderRepository(t)
21 tagRepo := repomocks.NewMockTagRepository(t)21 tagRepo := repomocks.NewMockTagRepository(t)
22 22
23 favorites := []*entity.Favorite{23 favorites := []*entity.Favorite{
@@ -28,7 +28,7 @@ func TestManageFavoritesUseCase_GetAllURLs_ReturnsURLSet(t *testing.T) {
28 28
29 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)29 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
30 30
31- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)31+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
32 32
33 urls, err := uc.GetAllURLs(ctx)33 urls, err := uc.GetAllURLs(ctx)
34 require.NoError(t, err)34 require.NoError(t, err)
@@ -47,12 +47,11 @@ func TestManageFavoritesUseCase_GetAllURLs_ReturnsEmptySetWhenNoFavorites(t *tes
47 ctx := testContext()47 ctx := testContext()
48 48
49 favoriteRepo := repomocks.NewMockFavoriteRepository(t)49 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
50- folderRepo := repomocks.NewMockFolderRepository(t)
51 tagRepo := repomocks.NewMockTagRepository(t)50 tagRepo := repomocks.NewMockTagRepository(t)
52 51
53 favoriteRepo.EXPECT().GetAll(mock.Anything).Return([]*entity.Favorite{}, nil)52 favoriteRepo.EXPECT().GetAll(mock.Anything).Return([]*entity.Favorite{}, nil)
54 53
55- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)54+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
56 55
57 urls, err := uc.GetAllURLs(ctx)56 urls, err := uc.GetAllURLs(ctx)
58 require.NoError(t, err)57 require.NoError(t, err)
@@ -64,12 +63,11 @@ func TestManageFavoritesUseCase_GetAllURLs_ReturnsErrorOnRepoFailure(t *testing.
64 ctx := testContext()63 ctx := testContext()
65 64
66 favoriteRepo := repomocks.NewMockFavoriteRepository(t)65 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
67- folderRepo := repomocks.NewMockFolderRepository(t)
68 tagRepo := repomocks.NewMockTagRepository(t)66 tagRepo := repomocks.NewMockTagRepository(t)
69 67
70 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(nil, errors.New("db connection failed"))68 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(nil, errors.New("db connection failed"))
71 69
72- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)70+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
73 71
74 urls, err := uc.GetAllURLs(ctx)72 urls, err := uc.GetAllURLs(ctx)
75 require.Error(t, err)73 require.Error(t, err)
@@ -81,7 +79,6 @@ func TestManageFavoritesUseCase_Toggle_AddsWhenNotFavorite(t *testing.T) {
81 ctx := testContext()79 ctx := testContext()
82 80
83 favoriteRepo := repomocks.NewMockFavoriteRepository(t)81 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
84- folderRepo := repomocks.NewMockFolderRepository(t)
85 tagRepo := repomocks.NewMockTagRepository(t)82 tagRepo := repomocks.NewMockTagRepository(t)
86 83
87 // URL is not a favorite84 // URL is not a favorite
@@ -90,7 +87,7 @@ func TestManageFavoritesUseCase_Toggle_AddsWhenNotFavorite(t *testing.T) {
90 return f.URL == "https://example.com" && f.Title == "Example"87 return f.URL == "https://example.com" && f.Title == "Example"
91 })).Return(nil)88 })).Return(nil)
92 89
93- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)90+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
94 91
95 result, err := uc.Toggle(ctx, "https://example.com", "Example")92 result, err := uc.Toggle(ctx, "https://example.com", "Example")
96 require.NoError(t, err)93 require.NoError(t, err)
@@ -106,7 +103,6 @@ func TestManageFavoritesUseCase_Toggle_RemovesWhenAlreadyFavorite(t *testing.T)
106 ctx := testContext()103 ctx := testContext()
107 104
108 favoriteRepo := repomocks.NewMockFavoriteRepository(t)105 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
109- folderRepo := repomocks.NewMockFolderRepository(t)
110 tagRepo := repomocks.NewMockTagRepository(t)106 tagRepo := repomocks.NewMockTagRepository(t)
111 107
112 existingFav := &entity.Favorite{108 existingFav := &entity.Favorite{
@@ -119,7 +115,7 @@ func TestManageFavoritesUseCase_Toggle_RemovesWhenAlreadyFavorite(t *testing.T)
119 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)115 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)
120 favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(nil)116 favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(nil)
121 117
122- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)118+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
123 119
124 result, err := uc.Toggle(ctx, "https://example.com", "Example")120 result, err := uc.Toggle(ctx, "https://example.com", "Example")
125 require.NoError(t, err)121 require.NoError(t, err)
@@ -135,10 +131,9 @@ func TestManageFavoritesUseCase_Toggle_EmptyURLReturnsError(t *testing.T) {
135 ctx := testContext()131 ctx := testContext()
136 132
137 favoriteRepo := repomocks.NewMockFavoriteRepository(t)133 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
138- folderRepo := repomocks.NewMockFolderRepository(t)
139 tagRepo := repomocks.NewMockTagRepository(t)134 tagRepo := repomocks.NewMockTagRepository(t)
140 135
141- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)136+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
142 137
143 result, err := uc.Toggle(ctx, "", "Title")138 result, err := uc.Toggle(ctx, "", "Title")
144 require.Error(t, err)139 require.Error(t, err)
@@ -150,7 +145,6 @@ func TestManageFavoritesUseCase_ToggleNormalizesURLBeforeLookup(t *testing.T) {
150 ctx := testContext()145 ctx := testContext()
151 146
152 favoriteRepo := repomocks.NewMockFavoriteRepository(t)147 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
153- folderRepo := repomocks.NewMockFolderRepository(t)
154 tagRepo := repomocks.NewMockTagRepository(t)148 tagRepo := repomocks.NewMockTagRepository(t)
155 149
156 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()150 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()
@@ -159,7 +153,7 @@ func TestManageFavoritesUseCase_ToggleNormalizesURLBeforeLookup(t *testing.T) {
159 return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example"153 return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example"
160 })).Return(nil).Once()154 })).Return(nil).Once()
161 155
162- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)156+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
163 157
164 result, err := uc.Toggle(ctx, " example.com ", " Example ")158 result, err := uc.Toggle(ctx, " example.com ", " Example ")
165 require.NoError(t, err)159 require.NoError(t, err)
@@ -173,12 +167,11 @@ func TestManageFavoritesUseCase_Toggle_FindByURLErrorReturnsError(t *testing.T)
173 ctx := testContext()167 ctx := testContext()
174 168
175 favoriteRepo := repomocks.NewMockFavoriteRepository(t)169 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
176- folderRepo := repomocks.NewMockFolderRepository(t)
177 tagRepo := repomocks.NewMockTagRepository(t)170 tagRepo := repomocks.NewMockTagRepository(t)
178 171
179 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, errors.New("db error"))172 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, errors.New("db error"))
180 173
181- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)174+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
182 175
183 result, err := uc.Toggle(ctx, "https://example.com", "Example")176 result, err := uc.Toggle(ctx, "https://example.com", "Example")
184 require.Error(t, err)177 require.Error(t, err)
@@ -190,13 +183,12 @@ func TestManageFavoritesUseCase_Toggle_SaveErrorReturnsError(t *testing.T) {
190 ctx := testContext()183 ctx := testContext()
191 184
192 favoriteRepo := repomocks.NewMockFavoriteRepository(t)185 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
193- folderRepo := repomocks.NewMockFolderRepository(t)
194 tagRepo := repomocks.NewMockTagRepository(t)186 tagRepo := repomocks.NewMockTagRepository(t)
195 187
196 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil)188 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil)
197 favoriteRepo.EXPECT().Save(mock.Anything, mock.Anything).Return(errors.New("db error"))189 favoriteRepo.EXPECT().Save(mock.Anything, mock.Anything).Return(errors.New("db error"))
198 190
199- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)191+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
200 192
201 result, err := uc.Toggle(ctx, "https://example.com", "Example")193 result, err := uc.Toggle(ctx, "https://example.com", "Example")
202 require.Error(t, err)194 require.Error(t, err)
@@ -208,14 +200,13 @@ func TestManageFavoritesUseCase_Toggle_DeleteErrorReturnsError(t *testing.T) {
208 ctx := testContext()200 ctx := testContext()
209 201
210 favoriteRepo := repomocks.NewMockFavoriteRepository(t)202 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
211- folderRepo := repomocks.NewMockFolderRepository(t)
212 tagRepo := repomocks.NewMockTagRepository(t)203 tagRepo := repomocks.NewMockTagRepository(t)
213 204
214 existingFav := &entity.Favorite{ID: 42, URL: "https://example.com", Title: "Example"}205 existingFav := &entity.Favorite{ID: 42, URL: "https://example.com", Title: "Example"}
215 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(existingFav, nil)206 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"))207 favoriteRepo.EXPECT().Delete(mock.Anything, entity.FavoriteID(42)).Return(errors.New("db error"))
217 208
218- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)209+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
219 210
220 result, err := uc.Toggle(ctx, "https://example.com", "Example")211 result, err := uc.Toggle(ctx, "https://example.com", "Example")
221 require.Error(t, err)212 require.Error(t, err)
@@ -227,7 +218,6 @@ func TestManageFavoritesUseCase_FilterForOmnibox_RanksPrefixBeforeContains(t *te
227 ctx := testContext()218 ctx := testContext()
228 219
229 favoriteRepo := repomocks.NewMockFavoriteRepository(t)220 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
230- folderRepo := repomocks.NewMockFolderRepository(t)
231 tagRepo := repomocks.NewMockTagRepository(t)221 tagRepo := repomocks.NewMockTagRepository(t)
232 222
233 favorites := []*entity.Favorite{223 favorites := []*entity.Favorite{
@@ -237,7 +227,7 @@ func TestManageFavoritesUseCase_FilterForOmnibox_RanksPrefixBeforeContains(t *te
237 }227 }
238 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)228 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
239 229
240- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)230+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
241 231
242 results, err := uc.FilterForOmnibox(ctx, "git")232 results, err := uc.FilterForOmnibox(ctx, "git")
243 require.NoError(t, err)233 require.NoError(t, err)
@@ -251,7 +241,6 @@ func TestManageFavoritesUseCase_FilterForOmnibox_EmptyQueryKeepsRepositoryOrder(
251 ctx := testContext()241 ctx := testContext()
252 242
253 favoriteRepo := repomocks.NewMockFavoriteRepository(t)243 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
254- folderRepo := repomocks.NewMockFolderRepository(t)
255 tagRepo := repomocks.NewMockTagRepository(t)244 tagRepo := repomocks.NewMockTagRepository(t)
256 245
257 favorites := []*entity.Favorite{246 favorites := []*entity.Favorite{
@@ -260,7 +249,7 @@ func TestManageFavoritesUseCase_FilterForOmnibox_EmptyQueryKeepsRepositoryOrder(
260 }249 }
261 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)250 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil)
262 251
263- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)252+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
264 253
265 results, err := uc.FilterForOmnibox(ctx, "")254 results, err := uc.FilterForOmnibox(ctx, "")
266 require.NoError(t, err)255 require.NoError(t, err)
@@ -273,7 +262,6 @@ func TestManageFavoritesUseCase_GetAllURLs_UsesCache(t *testing.T) {
273 ctx := testContext()262 ctx := testContext()
274 263
275 favoriteRepo := repomocks.NewMockFavoriteRepository(t)264 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
276- folderRepo := repomocks.NewMockFolderRepository(t)
277 tagRepo := repomocks.NewMockTagRepository(t)265 tagRepo := repomocks.NewMockTagRepository(t)
278 266
279 favorites := []*entity.Favorite{267 favorites := []*entity.Favorite{
@@ -281,7 +269,7 @@ func TestManageFavoritesUseCase_GetAllURLs_UsesCache(t *testing.T) {
281 }269 }
282 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil).Once()270 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil).Once()
283 271
284- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)272+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
285 273
286 urls, err := uc.GetAllURLs(ctx)274 urls, err := uc.GetAllURLs(ctx)
287 require.NoError(t, err)275 require.NoError(t, err)
@@ -296,7 +284,6 @@ func TestManageFavoritesUseCase_Toggle_InvalidatesCache(t *testing.T) {
296 ctx := testContext()284 ctx := testContext()
297 285
298 favoriteRepo := repomocks.NewMockFavoriteRepository(t)286 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
299- folderRepo := repomocks.NewMockFolderRepository(t)
300 tagRepo := repomocks.NewMockTagRepository(t)287 tagRepo := repomocks.NewMockTagRepository(t)
301 288
302 initialFavorites := []*entity.Favorite{289 initialFavorites := []*entity.Favorite{
@@ -313,7 +300,7 @@ func TestManageFavoritesUseCase_Toggle_InvalidatesCache(t *testing.T) {
313 }300 }
314 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(updatedFavorites, nil).Once()301 favoriteRepo.EXPECT().GetAll(mock.Anything).Return(updatedFavorites, nil).Once()
315 302
316- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)303+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
317 304
318 urls, err := uc.GetAllURLs(ctx)305 urls, err := uc.GetAllURLs(ctx)
319 require.NoError(t, err)306 require.NoError(t, err)
@@ -327,51 +314,10 @@ func TestManageFavoritesUseCase_Toggle_InvalidatesCache(t *testing.T) {
327 require.Contains(t, urls, "https://new.example")314 require.Contains(t, urls, "https://new.example")
328 }315 }
329 316
330-func 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-
370 func TestManageFavoritesUseCase_AddFavoriteNormalizesURL(t *testing.T) {317 func TestManageFavoritesUseCase_AddFavoriteNormalizesURL(t *testing.T) {
371 ctx := testContext()318 ctx := testContext()
372 319
373 favoriteRepo := repomocks.NewMockFavoriteRepository(t)320 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
374- folderRepo := repomocks.NewMockFolderRepository(t)
375 tagRepo := repomocks.NewMockTagRepository(t)321 tagRepo := repomocks.NewMockTagRepository(t)
376 322
377 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()323 favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()
@@ -380,7 +326,7 @@ func TestManageFavoritesUseCase_AddFavoriteNormalizesURL(t *testing.T) {
380 return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example"326 return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example"
381 })).Return(nil).Once()327 })).Return(nil).Once()
382 328
383- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)329+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
384 330
385 fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "example.com", Title: " Example "})331 fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "example.com", Title: " Example "})
386 require.NoError(t, err)332 require.NoError(t, err)
@@ -393,15 +339,42 @@ func TestManageFavoritesUseCase_AddFavoriteRejectsInvalidTagIDs(t *testing.T) {
393 ctx := testContext()339 ctx := testContext()
394 340
395 favoriteRepo := repomocks.NewMockFavoriteRepository(t)341 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
396- folderRepo := repomocks.NewMockFolderRepository(t)
397 tagRepo := repomocks.NewMockTagRepository(t)342 tagRepo := repomocks.NewMockTagRepository(t)
398- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)343+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
399 344
400 _, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "https://example.com", Tags: []entity.TagID{1, 0}})345 _, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "https://example.com", Tags: []entity.TagID{1, 0}})
401 require.Error(t, err)346 require.Error(t, err)
402 assert.Contains(t, err.Error(), "favorite tag id must be positive")347 assert.Contains(t, err.Error(), "favorite tag id must be positive")
403 }348 }
404 349
350+func TestManageFavoritesUseCase_AddFavoriteAssignsTagsWithoutFolder(t *testing.T) {
351+ ctx := testContext()
352+
353+ favoriteRepo := repomocks.NewMockFavoriteRepository(t)
354+ tagRepo := repomocks.NewMockTagRepository(t)
355+
356+ favoriteRepo.EXPECT().FindByURL(mock.Anything, "https://example.com").Return(nil, nil).Once()
357+ favoriteRepo.EXPECT().Save(mock.Anything, mock.MatchedBy(func(fav *entity.Favorite) bool {
358+ return fav != nil && fav.URL == "https://example.com" && fav.Title == "Example" && fav.FaviconURL == "https://example.com/favicon.ico"
359+ })).Run(func(_ context.Context, fav *entity.Favorite) {
360+ fav.ID = entity.FavoriteID(42)
361+ }).Return(nil).Once()
362+ tagRepo.EXPECT().AssignToFavorite(mock.Anything, entity.TagID(7), entity.FavoriteID(42)).Return(nil).Once()
363+ tagRepo.EXPECT().AssignToFavorite(mock.Anything, entity.TagID(8), entity.FavoriteID(42)).Return(nil).Once()
364+
365+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
366+
367+ fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{
368+ URL: "https://example.com",
369+ Title: "Example",
370+ FaviconURL: " https://example.com/favicon.ico ",
371+ Tags: []entity.TagID{7, 8},
372+ })
373+ require.NoError(t, err)
374+ require.NotNil(t, fav)
375+ assert.Equal(t, entity.FavoriteID(42), fav.ID)
376+}
377+
405 func TestManageFavoritesUseCase_AddFavoriteRejectsUnsafeOrMalformedURL(t *testing.T) {378 func TestManageFavoritesUseCase_AddFavoriteRejectsUnsafeOrMalformedURL(t *testing.T) {
406 ctx := testContext()379 ctx := testContext()
407 380
@@ -416,9 +389,8 @@ func TestManageFavoritesUseCase_AddFavoriteRejectsUnsafeOrMalformedURL(t *testin
416 for _, raw := range tests {389 for _, raw := range tests {
417 t.Run(raw, func(t *testing.T) {390 t.Run(raw, func(t *testing.T) {
418 favoriteRepo := repomocks.NewMockFavoriteRepository(t)391 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
419- folderRepo := repomocks.NewMockFolderRepository(t)
420 tagRepo := repomocks.NewMockTagRepository(t)392 tagRepo := repomocks.NewMockTagRepository(t)
421- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)393+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
422 394
423 _, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: raw})395 _, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: raw})
424 require.Error(t, err)396 require.Error(t, err)
@@ -430,7 +402,6 @@ func TestManageFavoritesUseCase_AddFavoriteNormalizesOpaqueDumbRoute(t *testing.
430 ctx := testContext()402 ctx := testContext()
431 403
432 favoriteRepo := repomocks.NewMockFavoriteRepository(t)404 favoriteRepo := repomocks.NewMockFavoriteRepository(t)
433- folderRepo := repomocks.NewMockFolderRepository(t)
434 tagRepo := repomocks.NewMockTagRepository(t)405 tagRepo := repomocks.NewMockTagRepository(t)
435 406
436 favoriteRepo.EXPECT().FindByURL(mock.Anything, "dumb://history").Return(nil, nil).Once()407 favoriteRepo.EXPECT().FindByURL(mock.Anything, "dumb://history").Return(nil, nil).Once()
@@ -439,7 +410,7 @@ func TestManageFavoritesUseCase_AddFavoriteNormalizesOpaqueDumbRoute(t *testing.
439 return fav != nil && fav.URL == "dumb://history" && fav.Title == "History"410 return fav != nil && fav.URL == "dumb://history" && fav.Title == "History"
440 })).Return(nil).Once()411 })).Return(nil).Once()
441 412
442- uc := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)413+ uc := usecase.NewManageFavoritesUseCase(favoriteRepo, tagRepo)
443 414
444 fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "dumb:history", Title: "History"})415 fav, err := uc.AddFavorite(ctx, dto.FavoriteCreateInput{URL: "dumb:history", Title: "History"})
445 require.NoError(t, err)416 require.NoError(t, err)

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