cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 563 lines 21 kB view raw
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}