1package sqlite_test
2
3import (
4 "context"
5 "path/filepath"
6 "testing"
7 "time"
8
9 "github.com/bnema/dumber/internal/domain/entity"
10 "github.com/bnema/dumber/internal/infrastructure/persistence/sqlite"
11 "github.com/bnema/dumber/internal/logging"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14)
15
16func historyTestCtx() context.Context {
17 logger := logging.NewFromConfigValues("debug", "console")
18 return logging.WithContext(context.Background(), logger)
19}
20
21func TestHistoryRepository_GetRecentSince(t *testing.T) {
22 ctx := historyTestCtx()
23 dbPath := filepath.Join(t.TempDir(), "dumber.db")
24
25 db, err := sqlite.NewConnection(ctx, dbPath)
26 require.NoError(t, err)
27 t.Cleanup(func() { _ = db.Close() })
28
29 repo := sqlite.NewHistoryRepository(db)
30
31 // Save some history entries
32 entries := []*entity.HistoryEntry{
33 {URL: "https://example.com", Title: "Example"},
34 {URL: "https://github.com", Title: "GitHub"},
35 {URL: "https://google.com", Title: "Google"},
36 }
37
38 for _, e := range entries {
39 require.NoError(t, repo.Save(ctx, e))
40 }
41
42 // Get recent since 30 days (all should be included)
43 results, err := repo.GetRecentSince(ctx, 30)
44 require.NoError(t, err)
45 require.Len(t, results, 3)
46
47 // Results should be sorted by last_visited DESC
48 // Since they were all just inserted, order may vary
49 urls := make(map[string]bool)
50 for _, r := range results {
51 urls[r.URL] = true
52 }
53 assert.True(t, urls["https://example.com"])
54 assert.True(t, urls["https://github.com"])
55 assert.True(t, urls["https://google.com"])
56}
57
58func TestHistoryRepository_GetRecentSince_EmptyResult(t *testing.T) {
59 ctx := historyTestCtx()
60 dbPath := filepath.Join(t.TempDir(), "dumber.db")
61
62 db, err := sqlite.NewConnection(ctx, dbPath)
63 require.NoError(t, err)
64 t.Cleanup(func() { _ = db.Close() })
65
66 repo := sqlite.NewHistoryRepository(db)
67
68 // Get recent with no data
69 results, err := repo.GetRecentSince(ctx, 30)
70 require.NoError(t, err)
71 assert.Empty(t, results)
72}
73
74func TestHistoryRepository_GetMostVisited(t *testing.T) {
75 ctx := historyTestCtx()
76 dbPath := filepath.Join(t.TempDir(), "dumber.db")
77
78 db, err := sqlite.NewConnection(ctx, dbPath)
79 require.NoError(t, err)
80 t.Cleanup(func() { _ = db.Close() })
81
82 repo := sqlite.NewHistoryRepository(db)
83
84 // Save entries with different visit counts
85 entry1 := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"}
86 entry2 := &entity.HistoryEntry{URL: "https://github.com", Title: "GitHub"}
87 entry3 := &entity.HistoryEntry{URL: "https://google.com", Title: "Google"}
88
89 require.NoError(t, repo.Save(ctx, entry1))
90 require.NoError(t, repo.Save(ctx, entry2))
91 require.NoError(t, repo.Save(ctx, entry3))
92
93 // Increment visit counts to create different popularities
94 // github: 5 visits, google: 3 visits, example: 1 visit
95 for i := 0; i < 4; i++ {
96 require.NoError(t, repo.IncrementVisitCount(ctx, "https://github.com"))
97 }
98 for i := 0; i < 2; i++ {
99 require.NoError(t, repo.IncrementVisitCount(ctx, "https://google.com"))
100 }
101
102 // Get most visited
103 results, err := repo.GetMostVisited(ctx, 30)
104 require.NoError(t, err)
105 require.Len(t, results, 3)
106
107 // First result should be github (5 visits)
108 assert.Equal(t, "https://github.com", results[0].URL)
109 assert.Equal(t, int64(5), results[0].VisitCount)
110
111 // Second should be google (3 visits)
112 assert.Equal(t, "https://google.com", results[1].URL)
113 assert.Equal(t, int64(3), results[1].VisitCount)
114
115 // Third should be example (1 visit)
116 assert.Equal(t, "https://example.com", results[2].URL)
117 assert.Equal(t, int64(1), results[2].VisitCount)
118}
119
120func TestHistoryRepository_GetMostVisited_EmptyResult(t *testing.T) {
121 ctx := historyTestCtx()
122 dbPath := filepath.Join(t.TempDir(), "dumber.db")
123
124 db, err := sqlite.NewConnection(ctx, dbPath)
125 require.NoError(t, err)
126 t.Cleanup(func() { _ = db.Close() })
127
128 repo := sqlite.NewHistoryRepository(db)
129
130 // Get most visited with no data
131 results, err := repo.GetMostVisited(ctx, 30)
132 require.NoError(t, err)
133 assert.Empty(t, results)
134}
135
136func TestHistoryRepository_GetRecentSince_RespectsTimeFilter(t *testing.T) {
137 ctx := historyTestCtx()
138 dbPath := filepath.Join(t.TempDir(), "dumber.db")
139
140 db, err := sqlite.NewConnection(ctx, dbPath)
141 require.NoError(t, err)
142 t.Cleanup(func() { _ = db.Close() })
143
144 repo := sqlite.NewHistoryRepository(db)
145
146 // Save an entry
147 entry := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"}
148 require.NoError(t, repo.Save(ctx, entry))
149
150 // Query with 1 day filter - should include recently added entry
151 results, err := repo.GetRecentSince(ctx, 1)
152 require.NoError(t, err)
153 require.Len(t, results, 1)
154
155 // Query with very large days filter - should still work
156 results, err = repo.GetRecentSince(ctx, 365)
157 require.NoError(t, err)
158 require.Len(t, results, 1)
159}
160
161func TestHistoryRepository_CRUD(t *testing.T) {
162 ctx := historyTestCtx()
163 dbPath := filepath.Join(t.TempDir(), "dumber.db")
164
165 db, err := sqlite.NewConnection(ctx, dbPath)
166 require.NoError(t, err)
167 t.Cleanup(func() { _ = db.Close() })
168
169 repo := sqlite.NewHistoryRepository(db)
170
171 // Create
172 entry := &entity.HistoryEntry{URL: "https://example.com", Title: "Example"}
173 require.NoError(t, repo.Save(ctx, entry))
174
175 // Read
176 found, err := repo.FindByURL(ctx, "https://example.com")
177 require.NoError(t, err)
178 require.NotNil(t, found)
179 assert.Equal(t, "https://example.com", found.URL)
180 assert.Equal(t, "Example", found.Title)
181 assert.Equal(t, int64(1), found.VisitCount)
182
183 // Update (save again increments visit count)
184 require.NoError(t, repo.Save(ctx, entry))
185 found2, err := repo.FindByURL(ctx, "https://example.com")
186 require.NoError(t, err)
187 assert.Equal(t, int64(2), found2.VisitCount)
188
189 // Get recent
190 recent, err := repo.GetRecent(ctx, 10, 0)
191 require.NoError(t, err)
192 require.Len(t, recent, 1)
193
194 // Delete
195 require.NoError(t, repo.Delete(ctx, found.ID))
196
197 // Verify deleted
198 deleted, err := repo.FindByURL(ctx, "https://example.com")
199 require.NoError(t, err)
200 assert.Nil(t, deleted)
201}
202
203// Ensure the test uses an isolated timestamp for time-sensitive queries
204func TestHistoryRepository_TimeFilterIntegrity(t *testing.T) {
205 ctx := historyTestCtx()
206 dbPath := filepath.Join(t.TempDir(), "dumber.db")
207
208 db, err := sqlite.NewConnection(ctx, dbPath)
209 require.NoError(t, err)
210 t.Cleanup(func() { _ = db.Close() })
211
212 repo := sqlite.NewHistoryRepository(db)
213
214 // Save entry
215 entry := &entity.HistoryEntry{URL: "https://test.com", Title: "Test"}
216 require.NoError(t, repo.Save(ctx, entry))
217
218 // Verify entry was saved with recent timestamp
219 found, err := repo.FindByURL(ctx, "https://test.com")
220 require.NoError(t, err)
221 require.NotNil(t, found)
222
223 // LastVisited should be within the last minute
224 timeDiff := time.Since(found.LastVisited)
225 assert.Less(t, timeDiff, time.Minute, "LastVisited should be recent, got %v ago", timeDiff)
226}
227
228func 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
249func 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
270func 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
295func 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}