fix(dmenu): implement days=0 as all history, add validation
- commit
- b782902290825a91d52364dbf1fb547f1deea3d0
- author
- bnema · · 2025-12-29 19:32 UTC
- parent
- cebf030
- signature
- GPG Unverified
2B82207D370127A0The key is unknown
10 files · +395 -14
M internal/application/usecase/search_history.go +29 -7
| @@ -76,26 +76,48 @@ func (uc *SearchHistoryUseCase) GetRecent(ctx context.Context, limit, offset int | |||
| 76 | 76 | } | |
| 77 | 77 | ||
| 78 | 78 | // GetRecentSince retrieves history entries visited within the last N days. | |
| 79 | + | // If days is 0, returns all history entries. | |
| 80 | + | // If days is negative, defaults to 30 days. | |
| 79 | 81 | func (uc *SearchHistoryUseCase) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 80 | - | if days <= 0 { | |
| 81 | - | days = 30 // Default to 30 days | |
| 82 | + | if days < 0 { | |
| 83 | + | days = 30 // Default to 30 days for negative values | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | var entries []*entity.HistoryEntry | |
| 87 | + | var err error | |
| 88 | + | ||
| 89 | + | switch days { | |
| 90 | + | case 0: | |
| 91 | + | entries, err = uc.historyRepo.GetAllRecentHistory(ctx) | |
| 92 | + | default: | |
| 93 | + | entries, err = uc.historyRepo.GetRecentSince(ctx, days) | |
| 82 | 94 | } | |
| 83 | 95 | ||
| 84 | - | entries, err := uc.historyRepo.GetRecentSince(ctx, days) | |
| 85 | 96 | if err != nil { | |
| 86 | - | return nil, fmt.Errorf("failed to get recent history since %d days: %w", days, err) | |
| 97 | + | return nil, fmt.Errorf("failed to get recent history: %w", err) | |
| 87 | 98 | } | |
| 88 | 99 | ||
| 89 | 100 | return entries, nil | |
| 90 | 101 | } | |
| 91 | 102 | ||
| 92 | 103 | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | |
| 104 | + | // If days is 0, returns all history entries sorted by visit count. | |
| 105 | + | // If days is negative, defaults to 30 days. | |
| 93 | 106 | func (uc *SearchHistoryUseCase) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 94 | - | if days <= 0 { | |
| 95 | - | days = 30 // Default to 30 days | |
| 107 | + | if days < 0 { | |
| 108 | + | days = 30 // Default to 30 days for negative values | |
| 109 | + | } | |
| 110 | + | ||
| 111 | + | var entries []*entity.HistoryEntry | |
| 112 | + | var err error | |
| 113 | + | ||
| 114 | + | switch days { | |
| 115 | + | case 0: | |
| 116 | + | entries, err = uc.historyRepo.GetAllMostVisited(ctx) | |
| 117 | + | default: | |
| 118 | + | entries, err = uc.historyRepo.GetMostVisited(ctx, days) | |
| 96 | 119 | } | |
| 97 | 120 | ||
| 98 | - | entries, err := uc.historyRepo.GetMostVisited(ctx, days) | |
| 99 | 121 | if err != nil { | |
| 100 | 122 | return nil, fmt.Errorf("failed to get most visited history: %w", err) | |
| 101 | 123 | } | |
| @@ -76,26 +76,48 @@ func (uc *SearchHistoryUseCase) GetRecent(ctx context.Context, limit, offset int | |||||
| 76 | } | 76 | } | ||
| 77 | 77 | ||||
| 78 | // GetRecentSince retrieves history entries visited within the last N days. | 78 | // GetRecentSince retrieves history entries visited within the last N days. | ||
| 79 | + | // If days is 0, returns all history entries. | |||
| 80 | + | // If days is negative, defaults to 30 days. | |||
| 79 | func (uc *SearchHistoryUseCase) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | 81 | func (uc *SearchHistoryUseCase) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | ||
| 80 | - | if days <= 0 { | 82 | + | if days < 0 { |
| 81 | - | days = 30 // Default to 30 days | 83 | + | days = 30 // Default to 30 days for negative values |
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | var entries []*entity.HistoryEntry | |||
| 87 | + | var err error | |||
| 88 | + | ||||
| 89 | + | switch days { | |||
| 90 | + | case 0: | |||
| 91 | + | entries, err = uc.historyRepo.GetAllRecentHistory(ctx) | |||
| 92 | + | default: | |||
| 93 | + | entries, err = uc.historyRepo.GetRecentSince(ctx, days) | |||
| 82 | } | 94 | } | ||
| 83 | 95 | ||||
| 84 | - | entries, err := uc.historyRepo.GetRecentSince(ctx, days) | |||
| 85 | if err != nil { | 96 | if err != nil { | ||
| 86 | - | return nil, fmt.Errorf("failed to get recent history since %d days: %w", days, err) | 97 | + | return nil, fmt.Errorf("failed to get recent history: %w", err) |
| 87 | } | 98 | } | ||
| 88 | 99 | ||||
| 89 | return entries, nil | 100 | return entries, nil | ||
| 90 | } | 101 | } | ||
| 91 | 102 | ||||
| 92 | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | 103 | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | ||
| 104 | + | // If days is 0, returns all history entries sorted by visit count. | |||
| 105 | + | // If days is negative, defaults to 30 days. | |||
| 93 | func (uc *SearchHistoryUseCase) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | 106 | func (uc *SearchHistoryUseCase) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | ||
| 94 | - | if days <= 0 { | 107 | + | if days < 0 { |
| 95 | - | days = 30 // Default to 30 days | 108 | + | days = 30 // Default to 30 days for negative values |
| 109 | + | } | |||
| 110 | + | ||||
| 111 | + | var entries []*entity.HistoryEntry | |||
| 112 | + | var err error | |||
| 113 | + | ||||
| 114 | + | switch days { | |||
| 115 | + | case 0: | |||
| 116 | + | entries, err = uc.historyRepo.GetAllMostVisited(ctx) | |||
| 117 | + | default: | |||
| 118 | + | entries, err = uc.historyRepo.GetMostVisited(ctx, days) | |||
| 96 | } | 119 | } | ||
| 97 | 120 | ||||
| 98 | - | entries, err := uc.historyRepo.GetMostVisited(ctx, days) | |||
| 99 | if err != nil { | 121 | if err != nil { | ||
| 100 | return nil, fmt.Errorf("failed to get most visited history: %w", err) | 122 | return nil, fmt.Errorf("failed to get most visited history: %w", err) | ||
| 101 | } | 123 | } | ||
M internal/application/usecase/search_history_test.go +7 -7
| @@ -34,7 +34,7 @@ func TestSearchHistoryUseCase_GetRecentSince_ReturnsEntries(t *testing.T) { | |||
| 34 | 34 | assert.Equal(t, "https://github.com", result[1].URL) | |
| 35 | 35 | } | |
| 36 | 36 | ||
| 37 | - | func TestSearchHistoryUseCase_GetRecentSince_DefaultsTo30Days(t *testing.T) { | |
| 37 | + | func TestSearchHistoryUseCase_GetRecentSince_ZeroMeansAll(t *testing.T) { | |
| 38 | 38 | ctx := testContext() | |
| 39 | 39 | ||
| 40 | 40 | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| @@ -43,8 +43,8 @@ func TestSearchHistoryUseCase_GetRecentSince_DefaultsTo30Days(t *testing.T) { | |||
| 43 | 43 | {ID: 1, URL: "https://example.com", Title: "Example"}, | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | - | // When days <= 0, should default to 30 | |
| 47 | - | historyRepo.EXPECT().GetRecentSince(mock.Anything, 30).Return(entries, nil) | |
| 46 | + | // When days == 0, should call GetAllRecentHistory | |
| 47 | + | historyRepo.EXPECT().GetAllRecentHistory(mock.Anything).Return(entries, nil) | |
| 48 | 48 | ||
| 49 | 49 | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 50 | 50 | ||
| @@ -81,7 +81,7 @@ func TestSearchHistoryUseCase_GetRecentSince_ReturnsErrorOnRepoFailure(t *testin | |||
| 81 | 81 | result, err := uc.GetRecentSince(ctx, 7) | |
| 82 | 82 | require.Error(t, err) | |
| 83 | 83 | assert.Nil(t, result) | |
| 84 | - | assert.Contains(t, err.Error(), "failed to get recent history since 7 days") | |
| 84 | + | assert.Contains(t, err.Error(), "failed to get recent history") | |
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | 87 | func TestSearchHistoryUseCase_GetMostVisited_ReturnsEntries(t *testing.T) { | |
| @@ -105,15 +105,15 @@ func TestSearchHistoryUseCase_GetMostVisited_ReturnsEntries(t *testing.T) { | |||
| 105 | 105 | assert.Equal(t, int64(100), result[0].VisitCount) | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | - | func TestSearchHistoryUseCase_GetMostVisited_DefaultsTo30Days(t *testing.T) { | |
| 108 | + | func TestSearchHistoryUseCase_GetMostVisited_ZeroMeansAll(t *testing.T) { | |
| 109 | 109 | ctx := testContext() | |
| 110 | 110 | ||
| 111 | 111 | historyRepo := repomocks.NewMockHistoryRepository(t) | |
| 112 | 112 | ||
| 113 | 113 | entries := []*entity.HistoryEntry{} | |
| 114 | 114 | ||
| 115 | - | // When days <= 0, should default to 30 | |
| 116 | - | historyRepo.EXPECT().GetMostVisited(mock.Anything, 30).Return(entries, nil) | |
| 115 | + | // When days == 0, should call GetAllMostVisited | |
| 116 | + | historyRepo.EXPECT().GetAllMostVisited(mock.Anything).Return(entries, nil) | |
| 117 | 117 | ||
| 118 | 118 | uc := usecase.NewSearchHistoryUseCase(historyRepo) | |
| 119 | 119 | ||
| @@ -34,7 +34,7 @@ func TestSearchHistoryUseCase_GetRecentSince_ReturnsEntries(t *testing.T) { | |||||
| 34 | assert.Equal(t, "https://github.com", result[1].URL) | 34 | assert.Equal(t, "https://github.com", result[1].URL) | ||
| 35 | } | 35 | } | ||
| 36 | 36 | ||||
| 37 | - | func TestSearchHistoryUseCase_GetRecentSince_DefaultsTo30Days(t *testing.T) { | 37 | + | func TestSearchHistoryUseCase_GetRecentSince_ZeroMeansAll(t *testing.T) { |
| 38 | ctx := testContext() | 38 | ctx := testContext() | ||
| 39 | 39 | ||||
| 40 | historyRepo := repomocks.NewMockHistoryRepository(t) | 40 | historyRepo := repomocks.NewMockHistoryRepository(t) | ||
| @@ -43,8 +43,8 @@ func TestSearchHistoryUseCase_GetRecentSince_DefaultsTo30Days(t *testing.T) { | |||||
| 43 | {ID: 1, URL: "https://example.com", Title: "Example"}, | 43 | {ID: 1, URL: "https://example.com", Title: "Example"}, | ||
| 44 | } | 44 | } | ||
| 45 | 45 | ||||
| 46 | - | // When days <= 0, should default to 30 | 46 | + | // When days == 0, should call GetAllRecentHistory |
| 47 | - | historyRepo.EXPECT().GetRecentSince(mock.Anything, 30).Return(entries, nil) | 47 | + | historyRepo.EXPECT().GetAllRecentHistory(mock.Anything).Return(entries, nil) |
| 48 | 48 | ||||
| 49 | uc := usecase.NewSearchHistoryUseCase(historyRepo) | 49 | uc := usecase.NewSearchHistoryUseCase(historyRepo) | ||
| 50 | 50 | ||||
| @@ -81,7 +81,7 @@ func TestSearchHistoryUseCase_GetRecentSince_ReturnsErrorOnRepoFailure(t *testin | |||||
| 81 | result, err := uc.GetRecentSince(ctx, 7) | 81 | result, err := uc.GetRecentSince(ctx, 7) | ||
| 82 | require.Error(t, err) | 82 | require.Error(t, err) | ||
| 83 | assert.Nil(t, result) | 83 | assert.Nil(t, result) | ||
| 84 | - | assert.Contains(t, err.Error(), "failed to get recent history since 7 days") | 84 | + | assert.Contains(t, err.Error(), "failed to get recent history") |
| 85 | } | 85 | } | ||
| 86 | 86 | ||||
| 87 | func TestSearchHistoryUseCase_GetMostVisited_ReturnsEntries(t *testing.T) { | 87 | func TestSearchHistoryUseCase_GetMostVisited_ReturnsEntries(t *testing.T) { | ||
| @@ -105,15 +105,15 @@ func TestSearchHistoryUseCase_GetMostVisited_ReturnsEntries(t *testing.T) { | |||||
| 105 | assert.Equal(t, int64(100), result[0].VisitCount) | 105 | assert.Equal(t, int64(100), result[0].VisitCount) | ||
| 106 | } | 106 | } | ||
| 107 | 107 | ||||
| 108 | - | func TestSearchHistoryUseCase_GetMostVisited_DefaultsTo30Days(t *testing.T) { | 108 | + | func TestSearchHistoryUseCase_GetMostVisited_ZeroMeansAll(t *testing.T) { |
| 109 | ctx := testContext() | 109 | ctx := testContext() | ||
| 110 | 110 | ||||
| 111 | historyRepo := repomocks.NewMockHistoryRepository(t) | 111 | historyRepo := repomocks.NewMockHistoryRepository(t) | ||
| 112 | 112 | ||||
| 113 | entries := []*entity.HistoryEntry{} | 113 | entries := []*entity.HistoryEntry{} | ||
| 114 | 114 | ||||
| 115 | - | // When days <= 0, should default to 30 | 115 | + | // When days == 0, should call GetAllMostVisited |
| 116 | - | historyRepo.EXPECT().GetMostVisited(mock.Anything, 30).Return(entries, nil) | 116 | + | historyRepo.EXPECT().GetAllMostVisited(mock.Anything).Return(entries, nil) |
| 117 | 117 | ||||
| 118 | uc := usecase.NewSearchHistoryUseCase(historyRepo) | 118 | uc := usecase.NewSearchHistoryUseCase(historyRepo) | ||
| 119 | 119 | ||||
M internal/domain/repository/history.go +8 -0
| @@ -22,11 +22,19 @@ type HistoryRepository interface { | |||
| 22 | 22 | GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | |
| 23 | 23 | ||
| 24 | 24 | // GetRecentSince retrieves history entries visited within the last N days. | |
| 25 | + | // days must be > 0. | |
| 25 | 26 | GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | |
| 26 | 27 | ||
| 27 | 28 | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | |
| 29 | + | // days must be > 0. | |
| 28 | 30 | GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | |
| 29 | 31 | ||
| 32 | + | // GetAllRecentHistory retrieves all history entries sorted by recency. | |
| 33 | + | GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) | |
| 34 | + | ||
| 35 | + | // GetAllMostVisited retrieves all history entries sorted by visit count. | |
| 36 | + | GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) | |
| 37 | + | ||
| 30 | 38 | // IncrementVisitCount increments the visit count for a URL. | |
| 31 | 39 | IncrementVisitCount(ctx context.Context, url string) error | |
| 32 | 40 | ||
| @@ -22,11 +22,19 @@ type HistoryRepository interface { | |||||
| 22 | GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | 22 | GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) | ||
| 23 | 23 | ||||
| 24 | // GetRecentSince retrieves history entries visited within the last N days. | 24 | // GetRecentSince retrieves history entries visited within the last N days. | ||
| 25 | + | // days must be > 0. | |||
| 25 | GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | 26 | GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | ||
| 26 | 27 | ||||
| 27 | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | 28 | // GetMostVisited retrieves history entries sorted by visit count within the last N days. | ||
| 29 | + | // days must be > 0. | |||
| 28 | GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | 30 | GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) | ||
| 29 | 31 | ||||
| 32 | + | // GetAllRecentHistory retrieves all history entries sorted by recency. | |||
| 33 | + | GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) | |||
| 34 | + | ||||
| 35 | + | // GetAllMostVisited retrieves all history entries sorted by visit count. | |||
| 36 | + | GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) | |||
| 37 | + | ||||
| 30 | // IncrementVisitCount increments the visit count for a URL. | 38 | // IncrementVisitCount increments the visit count for a URL. | ||
| 31 | IncrementVisitCount(ctx context.Context, url string) error | 39 | IncrementVisitCount(ctx context.Context, url string) error | ||
| 32 | 40 | ||||
M internal/domain/repository/mocks/mocks.go +124 -0
| @@ -2197,6 +2197,130 @@ func (_c *MockHistoryRepository_FindByURL_Call) RunAndReturn(run func(ctx contex | |||
| 2197 | 2197 | return _c | |
| 2198 | 2198 | } | |
| 2199 | 2199 | ||
| 2200 | + | // GetAllMostVisited provides a mock function for the type MockHistoryRepository | |
| 2201 | + | func (_mock *MockHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 2202 | + | ret := _mock.Called(ctx) | |
| 2203 | + | ||
| 2204 | + | if len(ret) == 0 { | |
| 2205 | + | panic("no return value specified for GetAllMostVisited") | |
| 2206 | + | } | |
| 2207 | + | ||
| 2208 | + | var r0 []*entity.HistoryEntry | |
| 2209 | + | var r1 error | |
| 2210 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*entity.HistoryEntry, error)); ok { | |
| 2211 | + | return returnFunc(ctx) | |
| 2212 | + | } | |
| 2213 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*entity.HistoryEntry); ok { | |
| 2214 | + | r0 = returnFunc(ctx) | |
| 2215 | + | } else { | |
| 2216 | + | if ret.Get(0) != nil { | |
| 2217 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |
| 2218 | + | } | |
| 2219 | + | } | |
| 2220 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 2221 | + | r1 = returnFunc(ctx) | |
| 2222 | + | } else { | |
| 2223 | + | r1 = ret.Error(1) | |
| 2224 | + | } | |
| 2225 | + | return r0, r1 | |
| 2226 | + | } | |
| 2227 | + | ||
| 2228 | + | // MockHistoryRepository_GetAllMostVisited_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllMostVisited' | |
| 2229 | + | type MockHistoryRepository_GetAllMostVisited_Call struct { | |
| 2230 | + | *mock.Call | |
| 2231 | + | } | |
| 2232 | + | ||
| 2233 | + | // GetAllMostVisited is a helper method to define mock.On call | |
| 2234 | + | // - ctx context.Context | |
| 2235 | + | func (_e *MockHistoryRepository_Expecter) GetAllMostVisited(ctx interface{}) *MockHistoryRepository_GetAllMostVisited_Call { | |
| 2236 | + | return &MockHistoryRepository_GetAllMostVisited_Call{Call: _e.mock.On("GetAllMostVisited", ctx)} | |
| 2237 | + | } | |
| 2238 | + | ||
| 2239 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) Run(run func(ctx context.Context)) *MockHistoryRepository_GetAllMostVisited_Call { | |
| 2240 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 2241 | + | var arg0 context.Context | |
| 2242 | + | if args[0] != nil { | |
| 2243 | + | arg0 = args[0].(context.Context) | |
| 2244 | + | } | |
| 2245 | + | run( | |
| 2246 | + | arg0, | |
| 2247 | + | ) | |
| 2248 | + | }) | |
| 2249 | + | return _c | |
| 2250 | + | } | |
| 2251 | + | ||
| 2252 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetAllMostVisited_Call { | |
| 2253 | + | _c.Call.Return(historyEntrys, err) | |
| 2254 | + | return _c | |
| 2255 | + | } | |
| 2256 | + | ||
| 2257 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) RunAndReturn(run func(ctx context.Context) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetAllMostVisited_Call { | |
| 2258 | + | _c.Call.Return(run) | |
| 2259 | + | return _c | |
| 2260 | + | } | |
| 2261 | + | ||
| 2262 | + | // GetAllRecentHistory provides a mock function for the type MockHistoryRepository | |
| 2263 | + | func (_mock *MockHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 2264 | + | ret := _mock.Called(ctx) | |
| 2265 | + | ||
| 2266 | + | if len(ret) == 0 { | |
| 2267 | + | panic("no return value specified for GetAllRecentHistory") | |
| 2268 | + | } | |
| 2269 | + | ||
| 2270 | + | var r0 []*entity.HistoryEntry | |
| 2271 | + | var r1 error | |
| 2272 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*entity.HistoryEntry, error)); ok { | |
| 2273 | + | return returnFunc(ctx) | |
| 2274 | + | } | |
| 2275 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*entity.HistoryEntry); ok { | |
| 2276 | + | r0 = returnFunc(ctx) | |
| 2277 | + | } else { | |
| 2278 | + | if ret.Get(0) != nil { | |
| 2279 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |
| 2280 | + | } | |
| 2281 | + | } | |
| 2282 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |
| 2283 | + | r1 = returnFunc(ctx) | |
| 2284 | + | } else { | |
| 2285 | + | r1 = ret.Error(1) | |
| 2286 | + | } | |
| 2287 | + | return r0, r1 | |
| 2288 | + | } | |
| 2289 | + | ||
| 2290 | + | // MockHistoryRepository_GetAllRecentHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllRecentHistory' | |
| 2291 | + | type MockHistoryRepository_GetAllRecentHistory_Call struct { | |
| 2292 | + | *mock.Call | |
| 2293 | + | } | |
| 2294 | + | ||
| 2295 | + | // GetAllRecentHistory is a helper method to define mock.On call | |
| 2296 | + | // - ctx context.Context | |
| 2297 | + | func (_e *MockHistoryRepository_Expecter) GetAllRecentHistory(ctx interface{}) *MockHistoryRepository_GetAllRecentHistory_Call { | |
| 2298 | + | return &MockHistoryRepository_GetAllRecentHistory_Call{Call: _e.mock.On("GetAllRecentHistory", ctx)} | |
| 2299 | + | } | |
| 2300 | + | ||
| 2301 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) Run(run func(ctx context.Context)) *MockHistoryRepository_GetAllRecentHistory_Call { | |
| 2302 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 2303 | + | var arg0 context.Context | |
| 2304 | + | if args[0] != nil { | |
| 2305 | + | arg0 = args[0].(context.Context) | |
| 2306 | + | } | |
| 2307 | + | run( | |
| 2308 | + | arg0, | |
| 2309 | + | ) | |
| 2310 | + | }) | |
| 2311 | + | return _c | |
| 2312 | + | } | |
| 2313 | + | ||
| 2314 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetAllRecentHistory_Call { | |
| 2315 | + | _c.Call.Return(historyEntrys, err) | |
| 2316 | + | return _c | |
| 2317 | + | } | |
| 2318 | + | ||
| 2319 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) RunAndReturn(run func(ctx context.Context) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetAllRecentHistory_Call { | |
| 2320 | + | _c.Call.Return(run) | |
| 2321 | + | return _c | |
| 2322 | + | } | |
| 2323 | + | ||
| 2200 | 2324 | // GetDailyVisitCount provides a mock function for the type MockHistoryRepository | |
| 2201 | 2325 | func (_mock *MockHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) { | |
| 2202 | 2326 | ret := _mock.Called(ctx, daysAgo) | |
| @@ -2197,6 +2197,130 @@ func (_c *MockHistoryRepository_FindByURL_Call) RunAndReturn(run func(ctx contex | |||||
| 2197 | return _c | 2197 | return _c | ||
| 2198 | } | 2198 | } | ||
| 2199 | 2199 | ||||
| 2200 | + | // GetAllMostVisited provides a mock function for the type MockHistoryRepository | |||
| 2201 | + | func (_mock *MockHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 2202 | + | ret := _mock.Called(ctx) | |||
| 2203 | + | ||||
| 2204 | + | if len(ret) == 0 { | |||
| 2205 | + | panic("no return value specified for GetAllMostVisited") | |||
| 2206 | + | } | |||
| 2207 | + | ||||
| 2208 | + | var r0 []*entity.HistoryEntry | |||
| 2209 | + | var r1 error | |||
| 2210 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*entity.HistoryEntry, error)); ok { | |||
| 2211 | + | return returnFunc(ctx) | |||
| 2212 | + | } | |||
| 2213 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*entity.HistoryEntry); ok { | |||
| 2214 | + | r0 = returnFunc(ctx) | |||
| 2215 | + | } else { | |||
| 2216 | + | if ret.Get(0) != nil { | |||
| 2217 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |||
| 2218 | + | } | |||
| 2219 | + | } | |||
| 2220 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |||
| 2221 | + | r1 = returnFunc(ctx) | |||
| 2222 | + | } else { | |||
| 2223 | + | r1 = ret.Error(1) | |||
| 2224 | + | } | |||
| 2225 | + | return r0, r1 | |||
| 2226 | + | } | |||
| 2227 | + | ||||
| 2228 | + | // MockHistoryRepository_GetAllMostVisited_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllMostVisited' | |||
| 2229 | + | type MockHistoryRepository_GetAllMostVisited_Call struct { | |||
| 2230 | + | *mock.Call | |||
| 2231 | + | } | |||
| 2232 | + | ||||
| 2233 | + | // GetAllMostVisited is a helper method to define mock.On call | |||
| 2234 | + | // - ctx context.Context | |||
| 2235 | + | func (_e *MockHistoryRepository_Expecter) GetAllMostVisited(ctx interface{}) *MockHistoryRepository_GetAllMostVisited_Call { | |||
| 2236 | + | return &MockHistoryRepository_GetAllMostVisited_Call{Call: _e.mock.On("GetAllMostVisited", ctx)} | |||
| 2237 | + | } | |||
| 2238 | + | ||||
| 2239 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) Run(run func(ctx context.Context)) *MockHistoryRepository_GetAllMostVisited_Call { | |||
| 2240 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 2241 | + | var arg0 context.Context | |||
| 2242 | + | if args[0] != nil { | |||
| 2243 | + | arg0 = args[0].(context.Context) | |||
| 2244 | + | } | |||
| 2245 | + | run( | |||
| 2246 | + | arg0, | |||
| 2247 | + | ) | |||
| 2248 | + | }) | |||
| 2249 | + | return _c | |||
| 2250 | + | } | |||
| 2251 | + | ||||
| 2252 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetAllMostVisited_Call { | |||
| 2253 | + | _c.Call.Return(historyEntrys, err) | |||
| 2254 | + | return _c | |||
| 2255 | + | } | |||
| 2256 | + | ||||
| 2257 | + | func (_c *MockHistoryRepository_GetAllMostVisited_Call) RunAndReturn(run func(ctx context.Context) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetAllMostVisited_Call { | |||
| 2258 | + | _c.Call.Return(run) | |||
| 2259 | + | return _c | |||
| 2260 | + | } | |||
| 2261 | + | ||||
| 2262 | + | // GetAllRecentHistory provides a mock function for the type MockHistoryRepository | |||
| 2263 | + | func (_mock *MockHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 2264 | + | ret := _mock.Called(ctx) | |||
| 2265 | + | ||||
| 2266 | + | if len(ret) == 0 { | |||
| 2267 | + | panic("no return value specified for GetAllRecentHistory") | |||
| 2268 | + | } | |||
| 2269 | + | ||||
| 2270 | + | var r0 []*entity.HistoryEntry | |||
| 2271 | + | var r1 error | |||
| 2272 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) ([]*entity.HistoryEntry, error)); ok { | |||
| 2273 | + | return returnFunc(ctx) | |||
| 2274 | + | } | |||
| 2275 | + | if returnFunc, ok := ret.Get(0).(func(context.Context) []*entity.HistoryEntry); ok { | |||
| 2276 | + | r0 = returnFunc(ctx) | |||
| 2277 | + | } else { | |||
| 2278 | + | if ret.Get(0) != nil { | |||
| 2279 | + | r0 = ret.Get(0).([]*entity.HistoryEntry) | |||
| 2280 | + | } | |||
| 2281 | + | } | |||
| 2282 | + | if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { | |||
| 2283 | + | r1 = returnFunc(ctx) | |||
| 2284 | + | } else { | |||
| 2285 | + | r1 = ret.Error(1) | |||
| 2286 | + | } | |||
| 2287 | + | return r0, r1 | |||
| 2288 | + | } | |||
| 2289 | + | ||||
| 2290 | + | // MockHistoryRepository_GetAllRecentHistory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllRecentHistory' | |||
| 2291 | + | type MockHistoryRepository_GetAllRecentHistory_Call struct { | |||
| 2292 | + | *mock.Call | |||
| 2293 | + | } | |||
| 2294 | + | ||||
| 2295 | + | // GetAllRecentHistory is a helper method to define mock.On call | |||
| 2296 | + | // - ctx context.Context | |||
| 2297 | + | func (_e *MockHistoryRepository_Expecter) GetAllRecentHistory(ctx interface{}) *MockHistoryRepository_GetAllRecentHistory_Call { | |||
| 2298 | + | return &MockHistoryRepository_GetAllRecentHistory_Call{Call: _e.mock.On("GetAllRecentHistory", ctx)} | |||
| 2299 | + | } | |||
| 2300 | + | ||||
| 2301 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) Run(run func(ctx context.Context)) *MockHistoryRepository_GetAllRecentHistory_Call { | |||
| 2302 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 2303 | + | var arg0 context.Context | |||
| 2304 | + | if args[0] != nil { | |||
| 2305 | + | arg0 = args[0].(context.Context) | |||
| 2306 | + | } | |||
| 2307 | + | run( | |||
| 2308 | + | arg0, | |||
| 2309 | + | ) | |||
| 2310 | + | }) | |||
| 2311 | + | return _c | |||
| 2312 | + | } | |||
| 2313 | + | ||||
| 2314 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) Return(historyEntrys []*entity.HistoryEntry, err error) *MockHistoryRepository_GetAllRecentHistory_Call { | |||
| 2315 | + | _c.Call.Return(historyEntrys, err) | |||
| 2316 | + | return _c | |||
| 2317 | + | } | |||
| 2318 | + | ||||
| 2319 | + | func (_c *MockHistoryRepository_GetAllRecentHistory_Call) RunAndReturn(run func(ctx context.Context) ([]*entity.HistoryEntry, error)) *MockHistoryRepository_GetAllRecentHistory_Call { | |||
| 2320 | + | _c.Call.Return(run) | |||
| 2321 | + | return _c | |||
| 2322 | + | } | |||
| 2323 | + | ||||
| 2200 | // GetDailyVisitCount provides a mock function for the type MockHistoryRepository | 2324 | // GetDailyVisitCount provides a mock function for the type MockHistoryRepository | ||
| 2201 | func (_mock *MockHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) { | 2325 | func (_mock *MockHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) { | ||
| 2202 | ret := _mock.Called(ctx, daysAgo) | 2326 | ret := _mock.Called(ctx, daysAgo) | ||
M internal/infrastructure/persistence/sqlite/history_repo.go +32 -0
| @@ -89,6 +89,9 @@ func (r *historyRepo) GetRecent(ctx context.Context, limit, offset int) ([]*enti | |||
| 89 | 89 | } | |
| 90 | 90 | ||
| 91 | 91 | func (r *historyRepo) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 92 | + | if days <= 0 { | |
| 93 | + | return nil, fmt.Errorf("days must be positive, got %d", days) | |
| 94 | + | } | |
| 92 | 95 | // Format: "-N days" for SQLite datetime modifier | |
| 93 | 96 | daysModifier := fmt.Sprintf("-%d days", days) | |
| 94 | 97 | rows, err := r.queries.GetRecentHistorySince(ctx, daysModifier) | |
| @@ -104,6 +107,9 @@ func (r *historyRepo) GetRecentSince(ctx context.Context, days int) ([]*entity.H | |||
| 104 | 107 | } | |
| 105 | 108 | ||
| 106 | 109 | func (r *historyRepo) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | |
| 110 | + | if days <= 0 { | |
| 111 | + | return nil, fmt.Errorf("days must be positive, got %d", days) | |
| 112 | + | } | |
| 107 | 113 | // Format: "-N days" for SQLite datetime modifier | |
| 108 | 114 | daysModifier := fmt.Sprintf("-%d days", days) | |
| 109 | 115 | rows, err := r.queries.GetMostVisited(ctx, daysModifier) | |
| @@ -118,6 +124,32 @@ func (r *historyRepo) GetMostVisited(ctx context.Context, days int) ([]*entity.H | |||
| 118 | 124 | return entries, nil | |
| 119 | 125 | } | |
| 120 | 126 | ||
| 127 | + | func (r *historyRepo) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 128 | + | rows, err := r.queries.GetAllRecentHistory(ctx) | |
| 129 | + | if err != nil { | |
| 130 | + | return nil, err | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |
| 134 | + | for i := range rows { | |
| 135 | + | entries[i] = historyFromRow(rows[i]) | |
| 136 | + | } | |
| 137 | + | return entries, nil | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func (r *historyRepo) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 141 | + | rows, err := r.queries.GetAllMostVisited(ctx) | |
| 142 | + | if err != nil { | |
| 143 | + | return nil, err | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |
| 147 | + | for i := range rows { | |
| 148 | + | entries[i] = historyFromRow(rows[i]) | |
| 149 | + | } | |
| 150 | + | return entries, nil | |
| 151 | + | } | |
| 152 | + | ||
| 121 | 153 | func (r *historyRepo) IncrementVisitCount(ctx context.Context, url string) error { | |
| 122 | 154 | return r.queries.IncrementVisitCount(ctx, url) | |
| 123 | 155 | } | |
| @@ -89,6 +89,9 @@ func (r *historyRepo) GetRecent(ctx context.Context, limit, offset int) ([]*enti | |||||
| 89 | } | 89 | } | ||
| 90 | 90 | ||||
| 91 | func (r *historyRepo) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | 91 | func (r *historyRepo) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | ||
| 92 | + | if days <= 0 { | |||
| 93 | + | return nil, fmt.Errorf("days must be positive, got %d", days) | |||
| 94 | + | } | |||
| 92 | // Format: "-N days" for SQLite datetime modifier | 95 | // Format: "-N days" for SQLite datetime modifier | ||
| 93 | daysModifier := fmt.Sprintf("-%d days", days) | 96 | daysModifier := fmt.Sprintf("-%d days", days) | ||
| 94 | rows, err := r.queries.GetRecentHistorySince(ctx, daysModifier) | 97 | rows, err := r.queries.GetRecentHistorySince(ctx, daysModifier) | ||
| @@ -104,6 +107,9 @@ func (r *historyRepo) GetRecentSince(ctx context.Context, days int) ([]*entity.H | |||||
| 104 | } | 107 | } | ||
| 105 | 108 | ||||
| 106 | func (r *historyRepo) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | 109 | func (r *historyRepo) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) { | ||
| 110 | + | if days <= 0 { | |||
| 111 | + | return nil, fmt.Errorf("days must be positive, got %d", days) | |||
| 112 | + | } | |||
| 107 | // Format: "-N days" for SQLite datetime modifier | 113 | // Format: "-N days" for SQLite datetime modifier | ||
| 108 | daysModifier := fmt.Sprintf("-%d days", days) | 114 | daysModifier := fmt.Sprintf("-%d days", days) | ||
| 109 | rows, err := r.queries.GetMostVisited(ctx, daysModifier) | 115 | rows, err := r.queries.GetMostVisited(ctx, daysModifier) | ||
| @@ -118,6 +124,32 @@ func (r *historyRepo) GetMostVisited(ctx context.Context, days int) ([]*entity.H | |||||
| 118 | return entries, nil | 124 | return entries, nil | ||
| 119 | } | 125 | } | ||
| 120 | 126 | ||||
| 127 | + | func (r *historyRepo) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 128 | + | rows, err := r.queries.GetAllRecentHistory(ctx) | |||
| 129 | + | if err != nil { | |||
| 130 | + | return nil, err | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |||
| 134 | + | for i := range rows { | |||
| 135 | + | entries[i] = historyFromRow(rows[i]) | |||
| 136 | + | } | |||
| 137 | + | return entries, nil | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func (r *historyRepo) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 141 | + | rows, err := r.queries.GetAllMostVisited(ctx) | |||
| 142 | + | if err != nil { | |||
| 143 | + | return nil, err | |||
| 144 | + | } | |||
| 145 | + | ||||
| 146 | + | entries := make([]*entity.HistoryEntry, len(rows)) | |||
| 147 | + | for i := range rows { | |||
| 148 | + | entries[i] = historyFromRow(rows[i]) | |||
| 149 | + | } | |||
| 150 | + | return entries, nil | |||
| 151 | + | } | |||
| 152 | + | ||||
| 121 | func (r *historyRepo) IncrementVisitCount(ctx context.Context, url string) error { | 153 | func (r *historyRepo) IncrementVisitCount(ctx context.Context, url string) error { | ||
| 122 | return r.queries.IncrementVisitCount(ctx, url) | 154 | return r.queries.IncrementVisitCount(ctx, url) | ||
| 123 | } | 155 | } | ||
M internal/infrastructure/persistence/sqlite/history_repo_test.go +99 -0
| @@ -224,3 +224,102 @@ func TestHistoryRepository_TimeFilterIntegrity(t *testing.T) { | |||
| 224 | 224 | timeDiff := time.Since(found.LastVisited) | |
| 225 | 225 | assert.Less(t, timeDiff, time.Minute, "LastVisited should be recent, got %v ago", timeDiff) | |
| 226 | 226 | } | |
| 227 | + | ||
| 228 | + | func TestHistoryRepository_GetRecentSince_RejectsNonPositiveDays(t *testing.T) { | |
| 229 | + | ctx := historyTestCtx() | |
| 230 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 231 | + | ||
| 232 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 233 | + | require.NoError(t, err) | |
| 234 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 235 | + | ||
| 236 | + | repo := sqlite.NewHistoryRepository(db) | |
| 237 | + | ||
| 238 | + | // days = 0 should return error | |
| 239 | + | _, err = repo.GetRecentSince(ctx, 0) | |
| 240 | + | require.Error(t, err) | |
| 241 | + | assert.Contains(t, err.Error(), "days must be positive") | |
| 242 | + | ||
| 243 | + | // days < 0 should return error | |
| 244 | + | _, err = repo.GetRecentSince(ctx, -5) | |
| 245 | + | require.Error(t, err) | |
| 246 | + | assert.Contains(t, err.Error(), "days must be positive") | |
| 247 | + | } | |
| 248 | + | ||
| 249 | + | func TestHistoryRepository_GetMostVisited_RejectsNonPositiveDays(t *testing.T) { | |
| 250 | + | ctx := historyTestCtx() | |
| 251 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 252 | + | ||
| 253 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 254 | + | require.NoError(t, err) | |
| 255 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 256 | + | ||
| 257 | + | repo := sqlite.NewHistoryRepository(db) | |
| 258 | + | ||
| 259 | + | // days = 0 should return error | |
| 260 | + | _, err = repo.GetMostVisited(ctx, 0) | |
| 261 | + | require.Error(t, err) | |
| 262 | + | assert.Contains(t, err.Error(), "days must be positive") | |
| 263 | + | ||
| 264 | + | // days < 0 should return error | |
| 265 | + | _, err = repo.GetMostVisited(ctx, -5) | |
| 266 | + | require.Error(t, err) | |
| 267 | + | assert.Contains(t, err.Error(), "days must be positive") | |
| 268 | + | } | |
| 269 | + | ||
| 270 | + | func TestHistoryRepository_GetAllRecentHistory(t *testing.T) { | |
| 271 | + | ctx := historyTestCtx() | |
| 272 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 273 | + | ||
| 274 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 275 | + | require.NoError(t, err) | |
| 276 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 277 | + | ||
| 278 | + | repo := sqlite.NewHistoryRepository(db) | |
| 279 | + | ||
| 280 | + | // Save some entries | |
| 281 | + | entries := []*entity.HistoryEntry{ | |
| 282 | + | {URL: "https://example.com", Title: "Example"}, | |
| 283 | + | {URL: "https://github.com", Title: "GitHub"}, | |
| 284 | + | } | |
| 285 | + | for _, e := range entries { | |
| 286 | + | require.NoError(t, repo.Save(ctx, e)) | |
| 287 | + | } | |
| 288 | + | ||
| 289 | + | // Get all recent history | |
| 290 | + | results, err := repo.GetAllRecentHistory(ctx) | |
| 291 | + | require.NoError(t, err) | |
| 292 | + | require.Len(t, results, 2) | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | func TestHistoryRepository_GetAllMostVisited(t *testing.T) { | |
| 296 | + | ctx := historyTestCtx() | |
| 297 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 298 | + | ||
| 299 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 300 | + | require.NoError(t, err) | |
| 301 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 302 | + | ||
| 303 | + | repo := sqlite.NewHistoryRepository(db) | |
| 304 | + | ||
| 305 | + | // Save entries with different visit counts | |
| 306 | + | entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |
| 307 | + | entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"} | |
| 308 | + | ||
| 309 | + | require.NoError(t, repo.Save(ctx, entry1)) | |
| 310 | + | require.NoError(t, repo.Save(ctx, entry2)) | |
| 311 | + | ||
| 312 | + | // Increment github visits | |
| 313 | + | for i := 0; i < 3; i++ { | |
| 314 | + | require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com")) | |
| 315 | + | } | |
| 316 | + | ||
| 317 | + | // Get all most visited | |
| 318 | + | results, err := repo.GetAllMostVisited(ctx) | |
| 319 | + | require.NoError(t, err) | |
| 320 | + | require.Len(t, results, 2) | |
| 321 | + | ||
| 322 | + | // First should be github (4 visits) | |
| 323 | + | assert.Equal(t, "https://github.com", results[0].URL) | |
| 324 | + | assert.Equal(t, int64(4), results[0].VisitCount) | |
| 325 | + | } | |
| @@ -224,3 +224,102 @@ func TestHistoryRepository_TimeFilterIntegrity(t *testing.T) { | |||||
| 224 | timeDiff := time.Since(found.LastVisited) | 224 | timeDiff := time.Since(found.LastVisited) | ||
| 225 | assert.Less(t, timeDiff, time.Minute, "LastVisited should be recent, got %v ago", timeDiff) | 225 | assert.Less(t, timeDiff, time.Minute, "LastVisited should be recent, got %v ago", timeDiff) | ||
| 226 | } | 226 | } | ||
| 227 | + | ||||
| 228 | + | func TestHistoryRepository_GetRecentSince_RejectsNonPositiveDays(t *testing.T) { | |||
| 229 | + | ctx := historyTestCtx() | |||
| 230 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 231 | + | ||||
| 232 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 233 | + | require.NoError(t, err) | |||
| 234 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 235 | + | ||||
| 236 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 237 | + | ||||
| 238 | + | // days = 0 should return error | |||
| 239 | + | _, err = repo.GetRecentSince(ctx, 0) | |||
| 240 | + | require.Error(t, err) | |||
| 241 | + | assert.Contains(t, err.Error(), "days must be positive") | |||
| 242 | + | ||||
| 243 | + | // days < 0 should return error | |||
| 244 | + | _, err = repo.GetRecentSince(ctx, -5) | |||
| 245 | + | require.Error(t, err) | |||
| 246 | + | assert.Contains(t, err.Error(), "days must be positive") | |||
| 247 | + | } | |||
| 248 | + | ||||
| 249 | + | func TestHistoryRepository_GetMostVisited_RejectsNonPositiveDays(t *testing.T) { | |||
| 250 | + | ctx := historyTestCtx() | |||
| 251 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 252 | + | ||||
| 253 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 254 | + | require.NoError(t, err) | |||
| 255 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 256 | + | ||||
| 257 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 258 | + | ||||
| 259 | + | // days = 0 should return error | |||
| 260 | + | _, err = repo.GetMostVisited(ctx, 0) | |||
| 261 | + | require.Error(t, err) | |||
| 262 | + | assert.Contains(t, err.Error(), "days must be positive") | |||
| 263 | + | ||||
| 264 | + | // days < 0 should return error | |||
| 265 | + | _, err = repo.GetMostVisited(ctx, -5) | |||
| 266 | + | require.Error(t, err) | |||
| 267 | + | assert.Contains(t, err.Error(), "days must be positive") | |||
| 268 | + | } | |||
| 269 | + | ||||
| 270 | + | func TestHistoryRepository_GetAllRecentHistory(t *testing.T) { | |||
| 271 | + | ctx := historyTestCtx() | |||
| 272 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 273 | + | ||||
| 274 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 275 | + | require.NoError(t, err) | |||
| 276 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 277 | + | ||||
| 278 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 279 | + | ||||
| 280 | + | // Save some entries | |||
| 281 | + | entries := []*entity.HistoryEntry{ | |||
| 282 | + | {URL: "https://example.com", Title: "Example"}, | |||
| 283 | + | {URL: "https://github.com", Title: "GitHub"}, | |||
| 284 | + | } | |||
| 285 | + | for _, e := range entries { | |||
| 286 | + | require.NoError(t, repo.Save(ctx, e)) | |||
| 287 | + | } | |||
| 288 | + | ||||
| 289 | + | // Get all recent history | |||
| 290 | + | results, err := repo.GetAllRecentHistory(ctx) | |||
| 291 | + | require.NoError(t, err) | |||
| 292 | + | require.Len(t, results, 2) | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | func TestHistoryRepository_GetAllMostVisited(t *testing.T) { | |||
| 296 | + | ctx := historyTestCtx() | |||
| 297 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 298 | + | ||||
| 299 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 300 | + | require.NoError(t, err) | |||
| 301 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 302 | + | ||||
| 303 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 304 | + | ||||
| 305 | + | // Save entries with different visit counts | |||
| 306 | + | entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"} | |||
| 307 | + | entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"} | |||
| 308 | + | ||||
| 309 | + | require.NoError(t, repo.Save(ctx, entry1)) | |||
| 310 | + | require.NoError(t, repo.Save(ctx, entry2)) | |||
| 311 | + | ||||
| 312 | + | // Increment github visits | |||
| 313 | + | for i := 0; i < 3; i++ { | |||
| 314 | + | require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com")) | |||
| 315 | + | } | |||
| 316 | + | ||||
| 317 | + | // Get all most visited | |||
| 318 | + | results, err := repo.GetAllMostVisited(ctx) | |||
| 319 | + | require.NoError(t, err) | |||
| 320 | + | require.Len(t, results, 2) | |||
| 321 | + | ||||
| 322 | + | // First should be github (4 visits) | |||
| 323 | + | assert.Equal(t, "https://github.com", results[0].URL) | |||
| 324 | + | assert.Equal(t, int64(4), results[0].VisitCount) | |||
| 325 | + | } | |||
M internal/infrastructure/persistence/sqlite/lazy_repos.go +14 -0
| @@ -93,6 +93,20 @@ func (r *LazyHistoryRepository) GetMostVisited(ctx context.Context, days int) ([ | |||
| 93 | 93 | return r.repo.GetMostVisited(ctx, days) | |
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | + | func (r *LazyHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 97 | + | if err := r.init(ctx); err != nil { | |
| 98 | + | return nil, err | |
| 99 | + | } | |
| 100 | + | return r.repo.GetAllRecentHistory(ctx) | |
| 101 | + | } | |
| 102 | + | ||
| 103 | + | func (r *LazyHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |
| 104 | + | if err := r.init(ctx); err != nil { | |
| 105 | + | return nil, err | |
| 106 | + | } | |
| 107 | + | return r.repo.GetAllMostVisited(ctx) | |
| 108 | + | } | |
| 109 | + | ||
| 96 | 110 | func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error { | |
| 97 | 111 | if err := r.init(ctx); err != nil { | |
| 98 | 112 | return err | |
| @@ -93,6 +93,20 @@ func (r *LazyHistoryRepository) GetMostVisited(ctx context.Context, days int) ([ | |||||
| 93 | return r.repo.GetMostVisited(ctx, days) | 93 | return r.repo.GetMostVisited(ctx, days) | ||
| 94 | } | 94 | } | ||
| 95 | 95 | ||||
| 96 | + | func (r *LazyHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 97 | + | if err := r.init(ctx); err != nil { | |||
| 98 | + | return nil, err | |||
| 99 | + | } | |||
| 100 | + | return r.repo.GetAllRecentHistory(ctx) | |||
| 101 | + | } | |||
| 102 | + | ||||
| 103 | + | func (r *LazyHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) { | |||
| 104 | + | if err := r.init(ctx); err != nil { | |||
| 105 | + | return nil, err | |||
| 106 | + | } | |||
| 107 | + | return r.repo.GetAllMostVisited(ctx) | |||
| 108 | + | } | |||
| 109 | + | ||||
| 96 | func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error { | 110 | func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error { | ||
| 97 | if err := r.init(ctx); err != nil { | 111 | if err := r.init(ctx); err != nil { | ||
| 98 | return err | 112 | return err | ||
M internal/infrastructure/persistence/sqlite/queries/history.sql +8 -0
| @@ -66,3 +66,11 @@ ORDER BY last_visited DESC; | |||
| 66 | 66 | SELECT * FROM history | |
| 67 | 67 | WHERE last_visited >= datetime('now', ?) | |
| 68 | 68 | ORDER BY visit_count DESC, last_visited DESC; | |
| 69 | + | ||
| 70 | + | -- name: GetAllRecentHistory :many | |
| 71 | + | SELECT * FROM history | |
| 72 | + | ORDER BY last_visited DESC; | |
| 73 | + | ||
| 74 | + | -- name: GetAllMostVisited :many | |
| 75 | + | SELECT * FROM history | |
| 76 | + | ORDER BY visit_count DESC, last_visited DESC; | |
| @@ -66,3 +66,11 @@ ORDER BY last_visited DESC; | |||||
| 66 | SELECT * FROM history | 66 | SELECT * FROM history | ||
| 67 | WHERE last_visited >= datetime('now', ?) | 67 | WHERE last_visited >= datetime('now', ?) | ||
| 68 | ORDER BY visit_count DESC, last_visited DESC; | 68 | ORDER BY visit_count DESC, last_visited DESC; | ||
| 69 | + | ||||
| 70 | + | -- name: GetAllRecentHistory :many | |||
| 71 | + | SELECT * FROM history | |||
| 72 | + | ORDER BY last_visited DESC; | |||
| 73 | + | ||||
| 74 | + | -- name: GetAllMostVisited :many | |||
| 75 | + | SELECT * FROM history | |||
| 76 | + | ORDER BY visit_count DESC, last_visited DESC; | |||
M internal/infrastructure/persistence/sqlite/sqlc/history.sql.go +72 -0
| @@ -58,6 +58,78 @@ func (q *Queries) DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.Nu | |||
| 58 | 58 | return err | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | + | const GetAllMostVisited = `-- name: GetAllMostVisited :many | |
| 62 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |
| 63 | + | ORDER BY visit_count DESC, last_visited DESC | |
| 64 | + | ` | |
| 65 | + | ||
| 66 | + | func (q *Queries) GetAllMostVisited(ctx context.Context) ([]History, error) { | |
| 67 | + | rows, err := q.db.QueryContext(ctx, GetAllMostVisited) | |
| 68 | + | if err != nil { | |
| 69 | + | return nil, err | |
| 70 | + | } | |
| 71 | + | defer rows.Close() | |
| 72 | + | items := []History{} | |
| 73 | + | for rows.Next() { | |
| 74 | + | var i History | |
| 75 | + | if err := rows.Scan( | |
| 76 | + | &i.ID, | |
| 77 | + | &i.Url, | |
| 78 | + | &i.Title, | |
| 79 | + | &i.FaviconUrl, | |
| 80 | + | &i.VisitCount, | |
| 81 | + | &i.LastVisited, | |
| 82 | + | &i.CreatedAt, | |
| 83 | + | ); err != nil { | |
| 84 | + | return nil, err | |
| 85 | + | } | |
| 86 | + | items = append(items, i) | |
| 87 | + | } | |
| 88 | + | if err := rows.Close(); err != nil { | |
| 89 | + | return nil, err | |
| 90 | + | } | |
| 91 | + | if err := rows.Err(); err != nil { | |
| 92 | + | return nil, err | |
| 93 | + | } | |
| 94 | + | return items, nil | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | const GetAllRecentHistory = `-- name: GetAllRecentHistory :many | |
| 98 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |
| 99 | + | ORDER BY last_visited DESC | |
| 100 | + | ` | |
| 101 | + | ||
| 102 | + | func (q *Queries) GetAllRecentHistory(ctx context.Context) ([]History, error) { | |
| 103 | + | rows, err := q.db.QueryContext(ctx, GetAllRecentHistory) | |
| 104 | + | if err != nil { | |
| 105 | + | return nil, err | |
| 106 | + | } | |
| 107 | + | defer rows.Close() | |
| 108 | + | items := []History{} | |
| 109 | + | for rows.Next() { | |
| 110 | + | var i History | |
| 111 | + | if err := rows.Scan( | |
| 112 | + | &i.ID, | |
| 113 | + | &i.Url, | |
| 114 | + | &i.Title, | |
| 115 | + | &i.FaviconUrl, | |
| 116 | + | &i.VisitCount, | |
| 117 | + | &i.LastVisited, | |
| 118 | + | &i.CreatedAt, | |
| 119 | + | ); err != nil { | |
| 120 | + | return nil, err | |
| 121 | + | } | |
| 122 | + | items = append(items, i) | |
| 123 | + | } | |
| 124 | + | if err := rows.Close(); err != nil { | |
| 125 | + | return nil, err | |
| 126 | + | } | |
| 127 | + | if err := rows.Err(); err != nil { | |
| 128 | + | return nil, err | |
| 129 | + | } | |
| 130 | + | return items, nil | |
| 131 | + | } | |
| 132 | + | ||
| 61 | 133 | const GetDailyVisitCount = `-- name: GetDailyVisitCount :many | |
| 62 | 134 | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC | |
| 63 | 135 | ` | |
| @@ -58,6 +58,78 @@ func (q *Queries) DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.Nu | |||||
| 58 | return err | 58 | return err | ||
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | + | const GetAllMostVisited = `-- name: GetAllMostVisited :many | |||
| 62 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |||
| 63 | + | ORDER BY visit_count DESC, last_visited DESC | |||
| 64 | + | ` | |||
| 65 | + | ||||
| 66 | + | func (q *Queries) GetAllMostVisited(ctx context.Context) ([]History, error) { | |||
| 67 | + | rows, err := q.db.QueryContext(ctx, GetAllMostVisited) | |||
| 68 | + | if err != nil { | |||
| 69 | + | return nil, err | |||
| 70 | + | } | |||
| 71 | + | defer rows.Close() | |||
| 72 | + | items := []History{} | |||
| 73 | + | for rows.Next() { | |||
| 74 | + | var i History | |||
| 75 | + | if err := rows.Scan( | |||
| 76 | + | &i.ID, | |||
| 77 | + | &i.Url, | |||
| 78 | + | &i.Title, | |||
| 79 | + | &i.FaviconUrl, | |||
| 80 | + | &i.VisitCount, | |||
| 81 | + | &i.LastVisited, | |||
| 82 | + | &i.CreatedAt, | |||
| 83 | + | ); err != nil { | |||
| 84 | + | return nil, err | |||
| 85 | + | } | |||
| 86 | + | items = append(items, i) | |||
| 87 | + | } | |||
| 88 | + | if err := rows.Close(); err != nil { | |||
| 89 | + | return nil, err | |||
| 90 | + | } | |||
| 91 | + | if err := rows.Err(); err != nil { | |||
| 92 | + | return nil, err | |||
| 93 | + | } | |||
| 94 | + | return items, nil | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | const GetAllRecentHistory = `-- name: GetAllRecentHistory :many | |||
| 98 | + | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history | |||
| 99 | + | ORDER BY last_visited DESC | |||
| 100 | + | ` | |||
| 101 | + | ||||
| 102 | + | func (q *Queries) GetAllRecentHistory(ctx context.Context) ([]History, error) { | |||
| 103 | + | rows, err := q.db.QueryContext(ctx, GetAllRecentHistory) | |||
| 104 | + | if err != nil { | |||
| 105 | + | return nil, err | |||
| 106 | + | } | |||
| 107 | + | defer rows.Close() | |||
| 108 | + | items := []History{} | |||
| 109 | + | for rows.Next() { | |||
| 110 | + | var i History | |||
| 111 | + | if err := rows.Scan( | |||
| 112 | + | &i.ID, | |||
| 113 | + | &i.Url, | |||
| 114 | + | &i.Title, | |||
| 115 | + | &i.FaviconUrl, | |||
| 116 | + | &i.VisitCount, | |||
| 117 | + | &i.LastVisited, | |||
| 118 | + | &i.CreatedAt, | |||
| 119 | + | ); err != nil { | |||
| 120 | + | return nil, err | |||
| 121 | + | } | |||
| 122 | + | items = append(items, i) | |||
| 123 | + | } | |||
| 124 | + | if err := rows.Close(); err != nil { | |||
| 125 | + | return nil, err | |||
| 126 | + | } | |||
| 127 | + | if err := rows.Err(); err != nil { | |||
| 128 | + | return nil, err | |||
| 129 | + | } | |||
| 130 | + | return items, nil | |||
| 131 | + | } | |||
| 132 | + | ||||
| 61 | const GetDailyVisitCount = `-- name: GetDailyVisitCount :many | 133 | const GetDailyVisitCount = `-- name: GetDailyVisitCount :many | ||
| 62 | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC | 134 | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC | ||
| 63 | ` | 135 | ` | ||
M internal/infrastructure/persistence/sqlite/sqlc/querier.go +2 -0
| @@ -32,6 +32,8 @@ type Querier interface { | |||
| 32 | 32 | GetActiveBrowserSession(ctx context.Context) (Session, error) | |
| 33 | 33 | GetAllFavorites(ctx context.Context) ([]Favorite, error) | |
| 34 | 34 | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | |
| 35 | + | GetAllMostVisited(ctx context.Context) ([]History, error) | |
| 36 | + | GetAllRecentHistory(ctx context.Context) ([]History, error) | |
| 35 | 37 | GetAllSessionStates(ctx context.Context) ([]SessionState, error) | |
| 36 | 38 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | |
| 37 | 39 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | |
| @@ -32,6 +32,8 @@ type Querier interface { | |||||
| 32 | GetActiveBrowserSession(ctx context.Context) (Session, error) | 32 | GetActiveBrowserSession(ctx context.Context) (Session, error) | ||
| 33 | GetAllFavorites(ctx context.Context) ([]Favorite, error) | 33 | GetAllFavorites(ctx context.Context) ([]Favorite, error) | ||
| 34 | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | 34 | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | ||
| 35 | + | GetAllMostVisited(ctx context.Context) ([]History, error) | |||
| 36 | + | GetAllRecentHistory(ctx context.Context) ([]History, error) | |||
| 35 | GetAllSessionStates(ctx context.Context) ([]SessionState, error) | 37 | GetAllSessionStates(ctx context.Context) ([]SessionState, error) | ||
| 36 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | 38 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | ||
| 37 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | 39 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | ||