cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package repo
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 _ "github.com/mattn/go-sqlite3"
10 "github.com/stormlightlabs/noteleaf/internal/models"
11 "github.com/stormlightlabs/noteleaf/internal/shared"
12)
13
14func TestArticleRepository(t *testing.T) {
15 t.Run("CRUD Operations", func(t *testing.T) {
16 ctx := context.Background()
17
18 t.Run("Create article", func(t *testing.T) {
19 db := CreateTestDB(t)
20 repo := NewArticleRepository(db)
21
22 article := CreateSampleArticle()
23 id, err := repo.Create(ctx, article)
24 shared.AssertNoError(t, err, "Failed to create article")
25 shared.AssertNotEqual(t, int64(0), id, "Expected non-zero ID")
26 shared.AssertEqual(t, id, article.ID, "Expected article ID to be set correctly")
27 shared.AssertFalse(t, article.Created.IsZero(), "Expected Created timestamp to be set")
28 shared.AssertFalse(t, article.Modified.IsZero(), "Expected Modified timestamp to be set")
29 })
30
31 t.Run("Get article", func(t *testing.T) {
32 db := CreateTestDB(t)
33 repo := NewArticleRepository(db)
34
35 original := CreateSampleArticle()
36 id, err := repo.Create(ctx, original)
37 shared.AssertNoError(t, err, "Failed to create article")
38
39 retrieved, err := repo.Get(ctx, id)
40 shared.AssertNoError(t, err, "Failed to get article")
41 shared.AssertEqual(t, original.ID, retrieved.ID, "ID mismatch")
42 shared.AssertEqual(t, original.URL, retrieved.URL, "URL mismatch")
43 shared.AssertEqual(t, original.Title, retrieved.Title, "Title mismatch")
44 shared.AssertEqual(t, original.Author, retrieved.Author, "Author mismatch")
45 shared.AssertEqual(t, original.Date, retrieved.Date, "Date mismatch")
46 shared.AssertEqual(t, original.MarkdownPath, retrieved.MarkdownPath, "MarkdownPath mismatch")
47 shared.AssertEqual(t, original.HTMLPath, retrieved.HTMLPath, "HTMLPath mismatch")
48 })
49
50 t.Run("Update article", func(t *testing.T) {
51 db := CreateTestDB(t)
52 repo := NewArticleRepository(db)
53
54 article := CreateSampleArticle()
55 id, err := repo.Create(ctx, article)
56 shared.AssertNoError(t, err, "Failed to create article")
57
58 originalModified := article.Modified
59 article.Title = "Updated Title"
60 article.Author = "Updated Author"
61 article.Date = "2024-01-02"
62 article.MarkdownPath = "/updated/path/article.md"
63 article.HTMLPath = "/updated/path/article.html"
64
65 err = repo.Update(ctx, article)
66 shared.AssertNoError(t, err, "Failed to update article")
67
68 retrieved, err := repo.Get(ctx, id)
69 shared.AssertNoError(t, err, "Failed to get updated article")
70 shared.AssertEqual(t, "Updated Title", retrieved.Title, "Expected updated title")
71 shared.AssertEqual(t, "Updated Author", retrieved.Author, "Expected updated author")
72 shared.AssertEqual(t, "2024-01-02", retrieved.Date, "Expected updated date")
73 shared.AssertEqual(t, "/updated/path/article.md", retrieved.MarkdownPath, "Expected updated markdown path")
74 shared.AssertEqual(t, "/updated/path/article.html", retrieved.HTMLPath, "Expected updated HTML path")
75 shared.AssertTrue(t, retrieved.Modified.After(originalModified), "Expected Modified timestamp to be updated")
76 })
77
78 t.Run("Delete article", func(t *testing.T) {
79 db := CreateTestDB(t)
80 repo := NewArticleRepository(db)
81
82 article := CreateSampleArticle()
83 id, err := repo.Create(ctx, article)
84 shared.AssertNoError(t, err, "Failed to create article")
85
86 err = repo.Delete(ctx, id)
87 shared.AssertNoError(t, err, "Failed to delete article")
88
89 _, err = repo.Get(ctx, id)
90 shared.AssertError(t, err, "Expected error when getting deleted article")
91 })
92 })
93
94 t.Run("Validation", func(t *testing.T) {
95 db := CreateTestDB(t)
96 repo := NewArticleRepository(db)
97 ctx := context.Background()
98
99 t.Run("Fails with missing title", func(t *testing.T) {
100 article := CreateSampleArticle()
101 article.Title = ""
102 _, err := repo.Create(ctx, article)
103 shared.AssertError(t, err, "Expected error when creating article with empty title")
104 })
105
106 t.Run("Fails with missing URL", func(t *testing.T) {
107 article := CreateSampleArticle()
108 article.URL = ""
109 _, err := repo.Create(ctx, article)
110 shared.AssertError(t, err, "Expected error when creating article with empty URL")
111 })
112
113 t.Run("Fails with duplicate URL", func(t *testing.T) {
114 article1 := CreateSampleArticle()
115 _, err := repo.Create(ctx, article1)
116 shared.AssertNoError(t, err, "Failed to create first article")
117
118 article2 := CreateSampleArticle()
119 article2.URL = article1.URL
120 _, err = repo.Create(ctx, article2)
121 shared.AssertError(t, err, "Expected error when creating article with duplicate URL")
122 })
123
124 t.Run("Fails with missing markdown path", func(t *testing.T) {
125 article := CreateSampleArticle()
126 article.MarkdownPath = ""
127 _, err := repo.Create(ctx, article)
128 shared.AssertError(t, err, "Expected error when creating article with empty markdown path")
129 shared.AssertContains(t, err.Error(), "MarkdownPath", "Expected MarkdownPath validation error")
130 })
131
132 t.Run("Fails with missing HTML path", func(t *testing.T) {
133 article := CreateSampleArticle()
134 article.HTMLPath = ""
135 _, err := repo.Create(ctx, article)
136 shared.AssertError(t, err, "Expected error when creating article with empty HTML path")
137 shared.AssertContains(t, err.Error(), "HTMLPath", "Expected HTMLPath validation error")
138 })
139
140 t.Run("Fails with invalid URL format", func(t *testing.T) {
141 article := CreateSampleArticle()
142 article.URL = "not-a-valid-url"
143 _, err := repo.Create(ctx, article)
144 shared.AssertError(t, err, "Expected error when creating article with invalid URL format")
145 shared.AssertContains(t, err.Error(), "URL", "Expected URL format validation error")
146 })
147
148 t.Run("Fails with invalid date format", func(t *testing.T) {
149 article := CreateSampleArticle()
150 article.Date = "invalid-date"
151 _, err := repo.Create(ctx, article)
152 shared.AssertError(t, err, "Expected error when creating article with invalid date format")
153 shared.AssertContains(t, err.Error(), "Date", "Expected date validation error")
154 })
155
156 t.Run("Fails with title too long", func(t *testing.T) {
157 article := CreateSampleArticle()
158 article.Title = strings.Repeat("a", 501)
159 _, err := repo.Create(ctx, article)
160 shared.AssertError(t, err, "Expected error when creating article with title too long")
161 shared.AssertContains(t, err.Error(), "Title", "Expected title length validation error")
162 })
163
164 t.Run("Fails with author too long", func(t *testing.T) {
165 article := CreateSampleArticle()
166 article.Author = strings.Repeat("a", 201)
167 _, err := repo.Create(ctx, article)
168 shared.AssertError(t, err, "Expected error when creating article with author too long")
169 shared.AssertContains(t, err.Error(), "Author", "Expected author length validation error")
170 })
171
172 t.Run("Validates timestamps", func(t *testing.T) {
173 article := CreateSampleArticle()
174 now := time.Now()
175 article.Modified = now
176 article.Created = now.Add(time.Hour)
177 err := repo.Validate(article)
178 shared.AssertError(t, err, "Expected error when created is after modified")
179 shared.AssertContains(t, err.Error(), "Created", "Expected timestamp validation error")
180 })
181
182 t.Run("Succeeds when created equals modified", func(t *testing.T) {
183 article := CreateSampleArticle()
184 now := time.Now()
185 article.Created = now
186 article.Modified = now
187 err := repo.Validate(article)
188 shared.AssertNoError(t, err, "Expected no error when created equals modified")
189 })
190
191 t.Run("Succeeds when created is before modified", func(t *testing.T) {
192 article := CreateSampleArticle()
193 now := time.Now()
194 article.Created = now
195 article.Modified = now.Add(time.Hour)
196 err := repo.Validate(article)
197 shared.AssertNoError(t, err, "Expected no error when created is before modified")
198 })
199
200 t.Run("Succeeds with valid optional fields", func(t *testing.T) {
201 article := CreateSampleArticle()
202 article.Date = "2024-01-01"
203 article.Author = "Test Author"
204 err := repo.Validate(article)
205 shared.AssertNoError(t, err, "Expected no error with valid optional fields")
206 })
207
208 t.Run("Succeeds with empty optional fields", func(t *testing.T) {
209 article := CreateSampleArticle()
210 article.Date = ""
211 article.Author = ""
212 err := repo.Validate(article)
213 shared.AssertNoError(t, err, "Expected no error with empty optional fields")
214 })
215 })
216
217 t.Run("GetByURL", func(t *testing.T) {
218 db := CreateTestDB(t)
219 repo := NewArticleRepository(db)
220 ctx := context.Background()
221
222 t.Run("Successfully retrieves article by URL", func(t *testing.T) {
223 original := CreateSampleArticle()
224 _, err := repo.Create(ctx, original)
225 shared.AssertNoError(t, err, "Failed to create article")
226
227 retrieved, err := repo.GetByURL(ctx, original.URL)
228 shared.AssertNoError(t, err, "Failed to get article by URL")
229 shared.AssertEqual(t, original.ID, retrieved.ID, "ID mismatch")
230 shared.AssertEqual(t, original.URL, retrieved.URL, "URL mismatch")
231 shared.AssertEqual(t, original.Title, retrieved.Title, "Title mismatch")
232 })
233
234 t.Run("Fails when URL not found", func(t *testing.T) {
235 nonexistent := "https://example.com/nonexistent"
236 _, err := repo.GetByURL(ctx, nonexistent)
237 shared.AssertError(t, err, "Expected error when getting article by non-existent URL")
238 shared.AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message")
239 })
240 })
241
242 t.Run("List", func(t *testing.T) {
243 db := CreateTestDB(t)
244 repo := NewArticleRepository(db)
245 ctx := context.Background()
246
247 articles := []*models.Article{
248 {
249 URL: "https://example.com/article1",
250 Title: "First Article",
251 Author: "John Doe",
252 Date: "2024-01-01",
253 MarkdownPath: "/path/article1.md",
254 HTMLPath: "/path/article1.html",
255 },
256 {
257 URL: "https://example.com/article2",
258 Title: "Second Article",
259 Author: "Jane Smith",
260 Date: "2024-01-02",
261 MarkdownPath: "/path/article2.md",
262 HTMLPath: "/path/article2.html",
263 },
264 {
265 URL: "https://different.com/article3",
266 Title: "Important Article",
267 Author: "John Doe",
268 Date: "2024-01-03",
269 MarkdownPath: "/path/article3.md",
270 HTMLPath: "/path/article3.html",
271 },
272 }
273
274 for _, article := range articles {
275 _, err := repo.Create(ctx, article)
276 shared.AssertNoError(t, err, "Failed to create test article")
277 }
278
279 t.Run("List all articles", func(t *testing.T) {
280 results, err := repo.List(ctx, nil)
281 shared.AssertNoError(t, err, "Failed to list all articles")
282 shared.AssertEqual(t, 3, len(results), "Expected 3 articles")
283 })
284
285 t.Run("Filter by title", func(t *testing.T) {
286 opts := &ArticleListOptions{Title: "Important"}
287 results, err := repo.List(ctx, opts)
288 shared.AssertNoError(t, err, "Failed to list articles by title")
289 shared.AssertEqual(t, 1, len(results), "Expected 1 article matching title")
290 shared.AssertEqual(t, "Important Article", results[0].Title, "Wrong article returned")
291 })
292
293 t.Run("Filter by author", func(t *testing.T) {
294 opts := &ArticleListOptions{Author: "John Doe"}
295 results, err := repo.List(ctx, opts)
296 shared.AssertNoError(t, err, "Failed to list articles by author")
297 shared.AssertEqual(t, 2, len(results), "Expected 2 articles by John Doe")
298 })
299
300 t.Run("Filter by URL", func(t *testing.T) {
301 opts := &ArticleListOptions{URL: "different.com"}
302 results, err := repo.List(ctx, opts)
303 shared.AssertNoError(t, err, "Failed to list articles by URL")
304 shared.AssertEqual(t, 1, len(results), "Expected 1 article from different.com")
305 })
306
307 t.Run("Filter by date range", func(t *testing.T) {
308 opts := &ArticleListOptions{DateFrom: "2024-01-02", DateTo: "2024-01-03"}
309 results, err := repo.List(ctx, opts)
310 shared.AssertNoError(t, err, "Failed to list articles by date range")
311 shared.AssertEqual(t, 2, len(results), "Expected 2 articles in date range")
312 })
313
314 t.Run("With limit", func(t *testing.T) {
315 opts := &ArticleListOptions{Limit: 2}
316 results, err := repo.List(ctx, opts)
317 shared.AssertNoError(t, err, "Failed to list articles with limit")
318 shared.AssertEqual(t, 2, len(results), "Expected 2 articles due to limit")
319 })
320
321 t.Run("With limit and offset", func(t *testing.T) {
322 opts := &ArticleListOptions{Limit: 2, Offset: 1}
323 results, err := repo.List(ctx, opts)
324 shared.AssertNoError(t, err, "Failed to list articles with limit and offset")
325 shared.AssertEqual(t, 2, len(results), "Expected 2 articles due to limit")
326 })
327
328 t.Run("Multiple filters", func(t *testing.T) {
329 opts := &ArticleListOptions{Author: "John Doe", DateFrom: "2024-01-02"}
330 results, err := repo.List(ctx, opts)
331 shared.AssertNoError(t, err, "Failed to list articles with multiple filters")
332 shared.AssertEqual(t, 1, len(results), "Expected 1 article matching all filters")
333 shared.AssertEqual(t, "Important Article", results[0].Title, "Wrong article returned")
334 })
335
336 t.Run("No results", func(t *testing.T) {
337 opts := &ArticleListOptions{Title: "Nonexistent"}
338 results, err := repo.List(ctx, opts)
339 shared.AssertNoError(t, err, "Failed to list articles")
340 shared.AssertEqual(t, 0, len(results), "Expected no articles")
341 })
342 })
343
344 t.Run("Count", func(t *testing.T) {
345 db := CreateTestDB(t)
346 repo := NewArticleRepository(db)
347 ctx := context.Background()
348
349 articles := []*models.Article{
350 CreateSampleArticle(),
351 {
352 URL: "https://example.com/article2",
353 Title: "Second Article",
354 Author: "Jane Smith",
355 Date: "2024-01-02",
356 MarkdownPath: "/path/article2.md",
357 HTMLPath: "/path/article2.html",
358 },
359 }
360
361 for _, article := range articles {
362 _, err := repo.Create(ctx, article)
363 shared.AssertNoError(t, err, "Failed to create test article")
364 }
365
366 t.Run("Count all articles", func(t *testing.T) {
367 count, err := repo.Count(ctx, nil)
368 shared.AssertNoError(t, err, "Failed to count articles")
369 shared.AssertEqual(t, int64(2), count, "Expected 2 articles")
370 })
371
372 t.Run("Count with filter", func(t *testing.T) {
373 opts := &ArticleListOptions{Author: "Test Author"}
374 count, err := repo.Count(ctx, opts)
375 shared.AssertNoError(t, err, "Failed to count articles with filter")
376 shared.AssertEqual(t, int64(1), count, "Expected 1 article by Test Author")
377 })
378
379 t.Run("Count with no results", func(t *testing.T) {
380 opts := &ArticleListOptions{Title: "Nonexistent"}
381 count, err := repo.Count(ctx, opts)
382 shared.AssertNoError(t, err, "Failed to count articles")
383 shared.AssertEqual(t, int64(0), count, "Expected 0 articles")
384 })
385 })
386
387 t.Run("Context Cancellation Error Paths", func(t *testing.T) {
388 db := CreateTestDB(t)
389 repo := NewArticleRepository(db)
390 ctx := context.Background()
391
392 article := CreateSampleArticle()
393 id, err := repo.Create(ctx, article)
394 shared.AssertNoError(t, err, "Failed to create article")
395
396 t.Run("Create with cancelled context", func(t *testing.T) {
397 newArticle := CreateSampleArticle()
398 _, err := repo.Create(NewCanceledContext(), newArticle)
399 AssertCancelledContext(t, err)
400 })
401
402 t.Run("Get with cancelled context", func(t *testing.T) {
403 _, err := repo.Get(NewCanceledContext(), id)
404 AssertCancelledContext(t, err)
405 })
406
407 t.Run("GetByURL with cancelled context", func(t *testing.T) {
408 _, err := repo.GetByURL(NewCanceledContext(), article.URL)
409 AssertCancelledContext(t, err)
410 })
411
412 t.Run("Update with cancelled context", func(t *testing.T) {
413 article.Title = "Updated"
414 err := repo.Update(NewCanceledContext(), article)
415 AssertCancelledContext(t, err)
416 })
417
418 t.Run("Delete with cancelled context", func(t *testing.T) {
419 err := repo.Delete(NewCanceledContext(), id)
420 AssertCancelledContext(t, err)
421 })
422
423 t.Run("List with cancelled context", func(t *testing.T) {
424 _, err := repo.List(NewCanceledContext(), nil)
425 AssertCancelledContext(t, err)
426 })
427
428 t.Run("Count with cancelled context", func(t *testing.T) {
429 _, err := repo.Count(NewCanceledContext(), nil)
430 AssertCancelledContext(t, err)
431 })
432 })
433
434 t.Run("Edge Cases", func(t *testing.T) {
435 db := CreateTestDB(t)
436 repo := NewArticleRepository(db)
437 ctx := context.Background()
438
439 t.Run("Get non-existent article", func(t *testing.T) {
440 _, err := repo.Get(ctx, 99999)
441 shared.AssertError(t, err, "Expected error for non-existent article")
442 shared.AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message")
443 })
444
445 t.Run("Update non-existent article", func(t *testing.T) {
446 article := CreateSampleArticle()
447 article.ID = 99999
448 err := repo.Update(ctx, article)
449 shared.AssertError(t, err, "Expected error when updating non-existent article")
450 shared.AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message")
451 })
452
453 t.Run("Delete non-existent article", func(t *testing.T) {
454 err := repo.Delete(ctx, 99999)
455 shared.AssertError(t, err, "Expected error when deleting non-existent article")
456 shared.AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message")
457 })
458
459 t.Run("Update validation - remove required title", func(t *testing.T) {
460 db := CreateTestDB(t)
461 repo := NewArticleRepository(db)
462
463 article := CreateSampleArticle()
464 _, err := repo.Create(ctx, article)
465 shared.AssertNoError(t, err, "Failed to create article")
466
467 article.Title = ""
468 err = repo.Update(ctx, article)
469 shared.AssertError(t, err, "Expected error when updating article with empty title")
470 })
471
472 t.Run("Update validation - invalid URL format", func(t *testing.T) {
473 db := CreateTestDB(t)
474 repo := NewArticleRepository(db)
475
476 article := CreateSampleArticle()
477 _, err := repo.Create(ctx, article)
478 shared.AssertNoError(t, err, "Failed to create article")
479
480 article.URL = "not-a-valid-url"
481 err = repo.Update(ctx, article)
482 shared.AssertError(t, err, "Expected error when updating article with invalid URL format")
483 shared.AssertContains(t, err.Error(), "URL", "Expected URL format validation error")
484 })
485
486 t.Run("Update validation - invalid date format", func(t *testing.T) {
487 db := CreateTestDB(t)
488 repo := NewArticleRepository(db)
489
490 article := CreateSampleArticle()
491 _, err := repo.Create(ctx, article)
492 shared.AssertNoError(t, err, "Failed to create article")
493
494 article.Date = "invalid-date"
495 err = repo.Update(ctx, article)
496 shared.AssertError(t, err, "Expected error when updating article with invalid date format")
497 shared.AssertContains(t, err.Error(), "Date", "Expected date validation error")
498 })
499
500 t.Run("Update validation - title too long", func(t *testing.T) {
501 db := CreateTestDB(t)
502 repo := NewArticleRepository(db)
503
504 article := CreateSampleArticle()
505 _, err := repo.Create(ctx, article)
506 shared.AssertNoError(t, err, "Failed to create article")
507
508 article.Title = strings.Repeat("a", 501)
509 err = repo.Update(ctx, article)
510 shared.AssertError(t, err, "Expected error when updating article with title too long")
511 shared.AssertContains(t, err.Error(), "Title", "Expected title length validation error")
512 })
513
514 t.Run("Update validation - author too long", func(t *testing.T) {
515 db := CreateTestDB(t)
516 repo := NewArticleRepository(db)
517
518 article := CreateSampleArticle()
519 _, err := repo.Create(ctx, article)
520 shared.AssertNoError(t, err, "Failed to create article")
521
522 article.Author = strings.Repeat("a", 201)
523 err = repo.Update(ctx, article)
524 shared.AssertError(t, err, "Expected error when updating article with author too long")
525 shared.AssertContains(t, err.Error(), "Author", "Expected author length validation error")
526 })
527
528 t.Run("Update validation - remove markdown path", func(t *testing.T) {
529 db := CreateTestDB(t)
530 repo := NewArticleRepository(db)
531
532 article := CreateSampleArticle()
533 _, err := repo.Create(ctx, article)
534 shared.AssertNoError(t, err, "Failed to create article")
535
536 article.MarkdownPath = ""
537 err = repo.Update(ctx, article)
538 shared.AssertError(t, err, "Expected error when updating article with empty markdown path")
539 shared.AssertContains(t, err.Error(), "MarkdownPath", "Expected MarkdownPath validation error")
540 })
541
542 t.Run("Update validation - remove HTML path", func(t *testing.T) {
543 db := CreateTestDB(t)
544 repo := NewArticleRepository(db)
545
546 article := CreateSampleArticle()
547 _, err := repo.Create(ctx, article)
548 shared.AssertNoError(t, err, "Failed to create article")
549
550 article.HTMLPath = ""
551 err = repo.Update(ctx, article)
552 shared.AssertError(t, err, "Expected error when updating article with empty HTML path")
553 shared.AssertContains(t, err.Error(), "HTMLPath", "Expected HTMLPath validation error")
554 })
555
556 t.Run("List with no results", func(t *testing.T) {
557 opts := &ArticleListOptions{Author: "NonExistentAuthor"}
558 articles, err := repo.List(ctx, opts)
559 shared.AssertNoError(t, err, "Should not error when no articles found")
560 shared.AssertEqual(t, 0, len(articles), "Expected empty result set")
561 })
562 })
563}