diff --git a/internal/handlers/test_utilities.go b/internal/handlers/test_utilities.go --- a/internal/handlers/test_utilities.go +++ b/internal/handlers/test_utilities.go @@ -1226,3 +1226,36 @@ t.Error("Task Modified timestamp should be set") } } + +// CreateBookHandler creates a [BookHandler] for testing with automatic cleanup +func CreateBookHandler(t *testing.T) *BookHandler { + t.Helper() + handler, err := NewBookHandler() + if err != nil { + t.Fatalf("Failed to create book handler: %v", err) + } + t.Cleanup(func() { handler.Close() }) + return handler +} + +// CreateMovieHandler creates a [MovieHandler] for testing with automatic cleanup +func CreateMovieHandler(t *testing.T) *MovieHandler { + t.Helper() + handler, err := NewMovieHandler() + if err != nil { + t.Fatalf("Failed to create movie handler: %v", err) + } + t.Cleanup(func() { handler.Close() }) + return handler +} + +// CreateTVHandler creates a [TVHandler] for testing with automatic cleanup +func CreateTVHandler(t *testing.T) *TVHandler { + t.Helper() + handler, err := NewTVHandler() + if err != nil { + t.Fatalf("Failed to create TV handler: %v", err) + } + t.Cleanup(func() { handler.Close() }) + return handler +} diff --git a/internal/repo/article_repository_test.go b/internal/repo/article_repository_test.go --- a/internal/repo/article_repository_test.go +++ b/internal/repo/article_repository_test.go @@ -11,250 +11,205 @@ ) func TestArticleRepository(t *testing.T) { - db := CreateTestDB(t) - repo := NewArticleRepository(db) - ctx := context.Background() - articles := CreateFakeArticles(10) - t.Run("CRUD Operations", func(t *testing.T) { - t.Run("Create", func(t *testing.T) { - t.Run("successfully creates an article", func(t *testing.T) { - article := articles[0] - id, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") - AssertNotEqual(t, int64(0), id, "Expected non-zero ID") - AssertEqual(t, id, article.ID, "Expected article ID to be set correctly") - AssertFalse(t, article.Created.IsZero(), "Expected Created timestamp to be set") - AssertFalse(t, article.Modified.IsZero(), "Expected Modified timestamp to be set") - }) + ctx := context.Background() - t.Run("Fails with missing title", func(t *testing.T) { - article := articles[1] - article.Title = "" - _, err := repo.Create(ctx, article) - AssertError(t, err, "Expected error when creating article with empty title") - }) + t.Run("Create article", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) - t.Run("Fails with missing URL", func(t *testing.T) { - article := articles[2] - article.URL = "" - _, err := repo.Create(ctx, article) - AssertError(t, err, "Expected error when creating article with empty URL") - }) - - t.Run("Fails with duplicate URL", func(t *testing.T) { - article1 := articles[0] - article2 := articles[1] - article2.URL = article1.URL - _, err := repo.Create(ctx, article2) - AssertError(t, err, "Expected error when creating article with duplicate URL") - }) - - t.Run("Fails with missing markdown path", func(t *testing.T) { - article := articles[3] - article.MarkdownPath = "" - _, err := repo.Create(ctx, article) - AssertError(t, err, "Expected error when creating article with empty markdown path") - AssertContains(t, err.Error(), "MarkdownPath", "Expected MarkdownPath validation error") - }) - - t.Run("Fails with missing HTML path", func(t *testing.T) { - article := articles[4] - article.HTMLPath = "" - _, err := repo.Create(ctx, article) - - AssertError(t, err, "Expected error when creating article with empty HTML path") - AssertContains(t, err.Error(), "HTMLPath", "Expected HTMLPath validation error") - }) - - t.Run("Fails with invalid URL format", func(t *testing.T) { - article := articles[5] - article.URL = "not-a-valid-url" - _, err := repo.Create(ctx, article) - AssertError(t, err, "Expected error when creating article with invalid URL format") - AssertContains(t, err.Error(), "URL", "Expected URL format validation error") - }) - - t.Run("Fails with invalid date format", func(t *testing.T) { - article := articles[6] - article.Date = "invalid-date" - _, err := repo.Create(ctx, article) - AssertError(t, err, "Expected error when creating article with invalid date format") - AssertContains(t, err.Error(), "Date", "Expected date validation error") - }) - - t.Run("Fails with title too long", func(t *testing.T) { - article := articles[7] - article.Title = strings.Repeat("a", 501) - _, err := repo.Create(ctx, article) - AssertError(t, err, "Expected error when creating article with title too long") - AssertContains(t, err.Error(), "Title", "Expected title length validation error") - }) - - t.Run("Fails with author too long", func(t *testing.T) { - article := articles[8] - article.Author = strings.Repeat("a", 201) - _, err := repo.Create(ctx, article) - AssertError(t, err, "Expected error when creating article with author too long") - AssertContains(t, err.Error(), "Author", "Expected author length validation error") - }) + article := CreateSampleArticle() + id, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") + AssertNotEqual(t, int64(0), id, "Expected non-zero ID") + AssertEqual(t, id, article.ID, "Expected article ID to be set correctly") + AssertFalse(t, article.Created.IsZero(), "Expected Created timestamp to be set") + AssertFalse(t, article.Modified.IsZero(), "Expected Modified timestamp to be set") }) - t.Run("Get", func(t *testing.T) { - t.Run("successfully retrieves an article", func(t *testing.T) { - original := CreateFakeArticle() - id, err := repo.Create(ctx, original) - AssertNoError(t, err, "Failed to create article") + t.Run("Get article", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) - retrieved, err := repo.Get(ctx, id) - AssertNoError(t, err, "Failed to get article") - AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") - AssertEqual(t, original.URL, retrieved.URL, "URL mismatch") - AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") - AssertEqual(t, original.Author, retrieved.Author, "Author mismatch") - AssertEqual(t, original.Date, retrieved.Date, "Date mismatch") - AssertEqual(t, original.MarkdownPath, retrieved.MarkdownPath, "MarkdownPath mismatch") - AssertEqual(t, original.HTMLPath, retrieved.HTMLPath, "HTMLPath mismatch") - }) + original := CreateSampleArticle() + id, err := repo.Create(ctx, original) + AssertNoError(t, err, "Failed to create article") - t.Run("Fails when ID isn't found", func(t *testing.T) { - nonExistentID := int64(99999) - _, err := repo.Get(ctx, nonExistentID) - AssertError(t, err, "Expected error when getting non-existent article") - AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") - }) + retrieved, err := repo.Get(ctx, id) + AssertNoError(t, err, "Failed to get article") + AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") + AssertEqual(t, original.URL, retrieved.URL, "URL mismatch") + AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") + AssertEqual(t, original.Author, retrieved.Author, "Author mismatch") + AssertEqual(t, original.Date, retrieved.Date, "Date mismatch") + AssertEqual(t, original.MarkdownPath, retrieved.MarkdownPath, "MarkdownPath mismatch") + AssertEqual(t, original.HTMLPath, retrieved.HTMLPath, "HTMLPath mismatch") }) - t.Run("Update", func(t *testing.T) { - t.Run("successfully updates an article", func(t *testing.T) { - article := CreateFakeArticle() - id, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") + t.Run("Update article", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) - originalModified := article.Modified - article.Title = "Updated Title" - article.Author = "Updated Author" - article.Date = "2024-01-02" - article.MarkdownPath = "/updated/path/article.md" - article.HTMLPath = "/updated/path/article.html" + article := CreateSampleArticle() + id, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") - err = repo.Update(ctx, article) - AssertNoError(t, err, "Failed to update article") + originalModified := article.Modified + article.Title = "Updated Title" + article.Author = "Updated Author" + article.Date = "2024-01-02" + article.MarkdownPath = "/updated/path/article.md" + article.HTMLPath = "/updated/path/article.html" - retrieved, err := repo.Get(ctx, id) - AssertNoError(t, err, "Failed to get updated article") - AssertEqual(t, "Updated Title", retrieved.Title, "Expected updated title") - AssertEqual(t, "Updated Author", retrieved.Author, "Expected updated author") - AssertEqual(t, "2024-01-02", retrieved.Date, "Expected updated date") - AssertEqual(t, "/updated/path/article.md", retrieved.MarkdownPath, "Expected updated markdown path") - AssertEqual(t, "/updated/path/article.html", retrieved.HTMLPath, "Expected updated HTML path") - AssertTrue(t, retrieved.Modified.After(originalModified), "Expected Modified timestamp to be updated") - }) + err = repo.Update(ctx, article) + AssertNoError(t, err, "Failed to update article") - t.Run("Fails when ID isn't found", func(t *testing.T) { - article := CreateFakeArticle() - article.ID = 99999 - err := repo.Update(ctx, article) - AssertError(t, err, "Expected error when updating non-existent article") - AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") - }) - - t.Run("Fails when trying to remove required value", func(t *testing.T) { - article := CreateFakeArticle() - _, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") - - article.Title = "" - err = repo.Update(ctx, article) - AssertError(t, err, "Expected error when updating article with empty title") - }) - - t.Run("Fails when setting invalid URL format", func(t *testing.T) { - article := CreateFakeArticle() - _, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") - - article.URL = "not-a-valid-url" - err = repo.Update(ctx, article) - AssertError(t, err, "Expected error when updating article with invalid URL format") - AssertContains(t, err.Error(), "URL", "Expected URL format validation error") - }) - - t.Run("Fails when setting invalid date format", func(t *testing.T) { - article := CreateFakeArticle() - _, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") - - article.Date = "invalid-date" - err = repo.Update(ctx, article) - AssertError(t, err, "Expected error when updating article with invalid date format") - AssertContains(t, err.Error(), "Date", "Expected date validation error") - }) - - t.Run("Fails when setting title too long", func(t *testing.T) { - article := CreateFakeArticle() - _, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") - - article.Title = strings.Repeat("a", 501) - err = repo.Update(ctx, article) - AssertError(t, err, "Expected error when updating article with title too long") - AssertContains(t, err.Error(), "Title", "Expected title length validation error") - }) - - t.Run("Fails when setting author too long", func(t *testing.T) { - article := CreateFakeArticle() - _, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") - - article.Author = strings.Repeat("a", 201) - err = repo.Update(ctx, article) - AssertError(t, err, "Expected error when updating article with author too long") - AssertContains(t, err.Error(), "Author", "Expected author length validation error") - }) - - t.Run("Fails when removing markdown path", func(t *testing.T) { - article := CreateFakeArticle() - _, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") - - article.MarkdownPath = "" - err = repo.Update(ctx, article) - AssertError(t, err, "Expected error when updating article with empty markdown path") - AssertContains(t, err.Error(), "MarkdownPath", "Expected MarkdownPath validation error") - }) - - t.Run("Fails when removing HTML path", func(t *testing.T) { - article := CreateFakeArticle() - _, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") - - article.HTMLPath = "" - err = repo.Update(ctx, article) - AssertError(t, err, "Expected error when updating article with empty HTML path") - AssertContains(t, err.Error(), "HTMLPath", "Expected HTMLPath validation error") - }) + retrieved, err := repo.Get(ctx, id) + AssertNoError(t, err, "Failed to get updated article") + AssertEqual(t, "Updated Title", retrieved.Title, "Expected updated title") + AssertEqual(t, "Updated Author", retrieved.Author, "Expected updated author") + AssertEqual(t, "2024-01-02", retrieved.Date, "Expected updated date") + AssertEqual(t, "/updated/path/article.md", retrieved.MarkdownPath, "Expected updated markdown path") + AssertEqual(t, "/updated/path/article.html", retrieved.HTMLPath, "Expected updated HTML path") + AssertTrue(t, retrieved.Modified.After(originalModified), "Expected Modified timestamp to be updated") }) - t.Run("Delete", func(t *testing.T) { - t.Run("successfully removes an article", func(t *testing.T) { - article := CreateFakeArticle() - id, err := repo.Create(ctx, article) - AssertNoError(t, err, "Failed to create article") + t.Run("Delete article", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) - err = repo.Delete(ctx, id) - AssertNoError(t, err, "Failed to delete article") + article := CreateSampleArticle() + id, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") - _, err = repo.Get(ctx, id) - AssertError(t, err, "Expected error when getting deleted article") - }) + err = repo.Delete(ctx, id) + AssertNoError(t, err, "Failed to delete article") - t.Run("Fails when ID isn't found", func(t *testing.T) { - nonexistent := int64(99999) - err := repo.Delete(ctx, nonexistent) - AssertError(t, err, "Expected error when deleting non-existent article") - AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") - }) + _, err = repo.Get(ctx, id) + AssertError(t, err, "Expected error when getting deleted article") + }) + }) + + t.Run("Validation", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + ctx := context.Background() + + t.Run("Fails with missing title", func(t *testing.T) { + article := CreateSampleArticle() + article.Title = "" + _, err := repo.Create(ctx, article) + AssertError(t, err, "Expected error when creating article with empty title") + }) + + t.Run("Fails with missing URL", func(t *testing.T) { + article := CreateSampleArticle() + article.URL = "" + _, err := repo.Create(ctx, article) + AssertError(t, err, "Expected error when creating article with empty URL") + }) + + t.Run("Fails with duplicate URL", func(t *testing.T) { + article1 := CreateSampleArticle() + _, err := repo.Create(ctx, article1) + AssertNoError(t, err, "Failed to create first article") + + article2 := CreateSampleArticle() + article2.URL = article1.URL + _, err = repo.Create(ctx, article2) + AssertError(t, err, "Expected error when creating article with duplicate URL") + }) + + t.Run("Fails with missing markdown path", func(t *testing.T) { + article := CreateSampleArticle() + article.MarkdownPath = "" + _, err := repo.Create(ctx, article) + AssertError(t, err, "Expected error when creating article with empty markdown path") + AssertContains(t, err.Error(), "MarkdownPath", "Expected MarkdownPath validation error") + }) + + t.Run("Fails with missing HTML path", func(t *testing.T) { + article := CreateSampleArticle() + article.HTMLPath = "" + _, err := repo.Create(ctx, article) + AssertError(t, err, "Expected error when creating article with empty HTML path") + AssertContains(t, err.Error(), "HTMLPath", "Expected HTMLPath validation error") + }) + + t.Run("Fails with invalid URL format", func(t *testing.T) { + article := CreateSampleArticle() + article.URL = "not-a-valid-url" + _, err := repo.Create(ctx, article) + AssertError(t, err, "Expected error when creating article with invalid URL format") + AssertContains(t, err.Error(), "URL", "Expected URL format validation error") + }) + + t.Run("Fails with invalid date format", func(t *testing.T) { + article := CreateSampleArticle() + article.Date = "invalid-date" + _, err := repo.Create(ctx, article) + AssertError(t, err, "Expected error when creating article with invalid date format") + AssertContains(t, err.Error(), "Date", "Expected date validation error") + }) + + t.Run("Fails with title too long", func(t *testing.T) { + article := CreateSampleArticle() + article.Title = strings.Repeat("a", 501) + _, err := repo.Create(ctx, article) + AssertError(t, err, "Expected error when creating article with title too long") + AssertContains(t, err.Error(), "Title", "Expected title length validation error") + }) + + t.Run("Fails with author too long", func(t *testing.T) { + article := CreateSampleArticle() + article.Author = strings.Repeat("a", 201) + _, err := repo.Create(ctx, article) + AssertError(t, err, "Expected error when creating article with author too long") + AssertContains(t, err.Error(), "Author", "Expected author length validation error") + }) + + t.Run("Validates timestamps", func(t *testing.T) { + article := CreateSampleArticle() + now := time.Now() + article.Modified = now + article.Created = now.Add(time.Hour) + err := repo.Validate(article) + AssertError(t, err, "Expected error when created is after modified") + AssertContains(t, err.Error(), "Created", "Expected timestamp validation error") + }) + + t.Run("Succeeds when created equals modified", func(t *testing.T) { + article := CreateSampleArticle() + now := time.Now() + article.Created = now + article.Modified = now + err := repo.Validate(article) + AssertNoError(t, err, "Expected no error when created equals modified") + }) + + t.Run("Succeeds when created is before modified", func(t *testing.T) { + article := CreateSampleArticle() + now := time.Now() + article.Created = now + article.Modified = now.Add(time.Hour) + err := repo.Validate(article) + AssertNoError(t, err, "Expected no error when created is before modified") + }) + + t.Run("Succeeds with valid optional fields", func(t *testing.T) { + article := CreateSampleArticle() + article.Date = "2024-01-01" + article.Author = "Test Author" + err := repo.Validate(article) + AssertNoError(t, err, "Expected no error with valid optional fields") + }) + + t.Run("Succeeds with empty optional fields", func(t *testing.T) { + article := CreateSampleArticle() + article.Date = "" + article.Author = "" + err := repo.Validate(article) + AssertNoError(t, err, "Expected no error with empty optional fields") }) }) @@ -263,8 +218,8 @@ repo := NewArticleRepository(db) ctx := context.Background() - t.Run("successfully retrieves an article by URL", func(t *testing.T) { - original := CreateFakeArticle() + t.Run("Successfully retrieves article by URL", func(t *testing.T) { + original := CreateSampleArticle() _, err := repo.Create(ctx, original) AssertNoError(t, err, "Failed to create article") @@ -275,7 +230,7 @@ AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") }) - t.Run("Fails when URL isn't found", func(t *testing.T) { + t.Run("Fails when URL not found", func(t *testing.T) { nonexistent := "https://example.com/nonexistent" _, err := repo.GetByURL(ctx, nonexistent) AssertError(t, err, "Expected error when getting article by non-existent URL") @@ -320,7 +275,7 @@ AssertNoError(t, err, "Failed to create test article") } - t.Run("All articles", func(t *testing.T) { + t.Run("List all articles", func(t *testing.T) { results, err := repo.List(ctx, nil) AssertNoError(t, err, "Failed to list all articles") AssertEqual(t, 3, len(results), "Expected 3 articles") @@ -390,13 +345,16 @@ repo := NewArticleRepository(db) ctx := context.Background() - articles := []*models.Article{CreateSampleArticle(), { - URL: "https://example.com/article2", - Title: "Second Article", - Author: "Jane Smith", - Date: "2024-01-02", - MarkdownPath: "/path/article2.md", - HTMLPath: "/path/article2.html"}, + articles := []*models.Article{ + CreateSampleArticle(), + { + URL: "https://example.com/article2", + Title: "Second Article", + Author: "Jane Smith", + Date: "2024-01-02", + MarkdownPath: "/path/article2.md", + HTMLPath: "/path/article2.html", + }, } for _, article := range articles { @@ -404,7 +362,7 @@ AssertNoError(t, err, "Failed to create test article") } - t.Run("Count all", func(t *testing.T) { + t.Run("Count all articles", func(t *testing.T) { count, err := repo.Count(ctx, nil) AssertNoError(t, err, "Failed to count articles") AssertEqual(t, int64(2), count, "Expected 2 articles") @@ -423,133 +381,182 @@ AssertNoError(t, err, "Failed to count articles") AssertEqual(t, int64(0), count, "Expected 0 articles") }) + }) - t.Run("Count with context cancellation", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(ctx) - cancel() + t.Run("Context Cancellation Error Paths", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + ctx := context.Background() - _, err := repo.Count(cancelCtx, nil) - if err == nil { - t.Error("Expected error with cancelled context") - } + article := CreateSampleArticle() + id, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") + + t.Run("Create with cancelled context", func(t *testing.T) { + newArticle := CreateSampleArticle() + _, err := repo.Create(NewCanceledContext(), newArticle) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Get with cancelled context", func(t *testing.T) { + _, err := repo.Get(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetByURL with cancelled context", func(t *testing.T) { + _, err := repo.GetByURL(NewCanceledContext(), article.URL) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Update with cancelled context", func(t *testing.T) { + article.Title = "Updated" + err := repo.Update(NewCanceledContext(), article) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Delete with cancelled context", func(t *testing.T) { + err := repo.Delete(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("List with cancelled context", func(t *testing.T) { + _, err := repo.List(NewCanceledContext(), nil) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Count with cancelled context", func(t *testing.T) { + _, err := repo.Count(NewCanceledContext(), nil) + AssertError(t, err, "Expected error with cancelled context") }) }) - t.Run("Validate", func(t *testing.T) { - repo := NewArticleRepository(CreateTestDB(t)) + t.Run("Edge Cases", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + ctx := context.Background() - t.Run("successfully validates a valid article", func(t *testing.T) { - article := CreateSampleArticle() - err := repo.Validate(article) - AssertNoError(t, err, "Expected no validation errors for valid article") + t.Run("Get non-existent article", func(t *testing.T) { + _, err := repo.Get(ctx, 99999) + AssertError(t, err, "Expected error for non-existent article") + AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") }) - t.Run("fails with missing required URL", func(t *testing.T) { + t.Run("Update non-existent article", func(t *testing.T) { article := CreateSampleArticle() - article.URL = "" - err := repo.Validate(article) - AssertError(t, err, "Expected error for missing URL") - AssertContains(t, err.Error(), "URL", "Expected URL validation error") + article.ID = 99999 + err := repo.Update(ctx, article) + AssertError(t, err, "Expected error when updating non-existent article") + AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") }) - t.Run("fails with missing required title", func(t *testing.T) { + t.Run("Delete non-existent article", func(t *testing.T) { + err := repo.Delete(ctx, 99999) + AssertError(t, err, "Expected error when deleting non-existent article") + AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") + }) + + t.Run("Update validation - remove required title", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + article := CreateSampleArticle() + _, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") + article.Title = "" - err := repo.Validate(article) - AssertError(t, err, "Expected error for missing title") - AssertContains(t, err.Error(), "Title", "Expected title validation error") + err = repo.Update(ctx, article) + AssertError(t, err, "Expected error when updating article with empty title") }) - t.Run("fails with missing markdown path", func(t *testing.T) { - article := CreateSampleArticle() - article.MarkdownPath = "" - err := repo.Validate(article) - AssertError(t, err, "Expected error for missing markdown path") - AssertContains(t, err.Error(), "MarkdownPath", "Expected markdown path validation error") - }) + t.Run("Update validation - invalid URL format", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) - t.Run("fails with missing HTML path", func(t *testing.T) { article := CreateSampleArticle() - article.HTMLPath = "" - err := repo.Validate(article) - AssertError(t, err, "Expected error for missing HTML path") - AssertContains(t, err.Error(), "HTMLPath", "Expected HTML path validation error") - }) + _, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") - t.Run("fails with invalid URL format", func(t *testing.T) { - article := CreateSampleArticle() article.URL = "not-a-valid-url" - err := repo.Validate(article) - AssertError(t, err, "Expected error for invalid URL format") + err = repo.Update(ctx, article) + AssertError(t, err, "Expected error when updating article with invalid URL format") AssertContains(t, err.Error(), "URL", "Expected URL format validation error") }) - t.Run("fails with invalid date format", func(t *testing.T) { + t.Run("Update validation - invalid date format", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + article := CreateSampleArticle() + _, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") + article.Date = "invalid-date" - err := repo.Validate(article) - AssertError(t, err, "Expected error for invalid date format") + err = repo.Update(ctx, article) + AssertError(t, err, "Expected error when updating article with invalid date format") AssertContains(t, err.Error(), "Date", "Expected date validation error") }) - t.Run("fails with title too long", func(t *testing.T) { + t.Run("Update validation - title too long", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + article := CreateSampleArticle() + _, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") + article.Title = strings.Repeat("a", 501) - err := repo.Validate(article) - AssertError(t, err, "Expected error for title too long") + err = repo.Update(ctx, article) + AssertError(t, err, "Expected error when updating article with title too long") AssertContains(t, err.Error(), "Title", "Expected title length validation error") }) - t.Run("fails with author too long", func(t *testing.T) { + t.Run("Update validation - author too long", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + article := CreateSampleArticle() + _, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") + article.Author = strings.Repeat("a", 201) - err := repo.Validate(article) - AssertError(t, err, "Expected error for author too long") + err = repo.Update(ctx, article) + AssertError(t, err, "Expected error when updating article with author too long") AssertContains(t, err.Error(), "Author", "Expected author length validation error") }) - t.Run("fails when created is after modified", func(t *testing.T) { + t.Run("Update validation - remove markdown path", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + article := CreateSampleArticle() - now := time.Now() - article.Modified = now - article.Created = now.Add(time.Hour) - err := repo.Validate(article) - AssertError(t, err, "Expected error when created is after modified") - AssertContains(t, err.Error(), "Created", "Expected timestamp validation error") + _, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") + + article.MarkdownPath = "" + err = repo.Update(ctx, article) + AssertError(t, err, "Expected error when updating article with empty markdown path") + AssertContains(t, err.Error(), "MarkdownPath", "Expected MarkdownPath validation error") }) - t.Run("succeeds when created equals modified", func(t *testing.T) { + t.Run("Update validation - remove HTML path", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewArticleRepository(db) + article := CreateSampleArticle() - now := time.Now() - article.Created = now - article.Modified = now - err := repo.Validate(article) - AssertNoError(t, err, "Expected no error when created equals modified") + _, err := repo.Create(ctx, article) + AssertNoError(t, err, "Failed to create article") + + article.HTMLPath = "" + err = repo.Update(ctx, article) + AssertError(t, err, "Expected error when updating article with empty HTML path") + AssertContains(t, err.Error(), "HTMLPath", "Expected HTMLPath validation error") }) - t.Run("succeeds when created is before modified", func(t *testing.T) { - article := CreateSampleArticle() - now := time.Now() - article.Created = now - article.Modified = now.Add(time.Hour) - err := repo.Validate(article) - AssertNoError(t, err, "Expected no error when created is before modified") - }) - - t.Run("succeeds with valid optional fields", func(t *testing.T) { - article := CreateSampleArticle() - article.Date = "2024-01-01" - article.Author = "Test Author" - err := repo.Validate(article) - AssertNoError(t, err, "Expected no error with valid optional fields") - }) - - t.Run("succeeds with empty optional fields", func(t *testing.T) { - article := CreateSampleArticle() - article.Date = "" - article.Author = "" - err := repo.Validate(article) - AssertNoError(t, err, "Expected no error with empty optional fields") + t.Run("List with no results", func(t *testing.T) { + opts := &ArticleListOptions{Author: "NonExistentAuthor"} + articles, err := repo.List(ctx, opts) + AssertNoError(t, err, "Should not error when no articles found") + AssertEqual(t, 0, len(articles), "Expected empty result set") }) }) } diff --git a/internal/repo/book_repository_test.go b/internal/repo/book_repository_test.go --- a/internal/repo/book_repository_test.go +++ b/internal/repo/book_repository_test.go @@ -315,13 +315,124 @@ }) t.Run("Count with context cancellation", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(ctx) - cancel() + _, err := repo.Count(NewCanceledContext(), BookListOptions{}) + AssertError(t, err, "Expected error with cancelled context") + }) + }) - _, err := repo.Count(cancelCtx, BookListOptions{}) - if err == nil { - t.Error("Expected error with cancelled context") - } + t.Run("Context Cancellation Error Paths", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewBookRepository(db) + ctx := context.Background() + + book := NewBookBuilder().WithTitle("Test Book").WithAuthor("Test Author").Build() + id, err := repo.Create(ctx, book) + AssertNoError(t, err, "Failed to create book") + + t.Run("Create with cancelled context", func(t *testing.T) { + newBook := NewBookBuilder().WithTitle("Cancelled").Build() + _, err := repo.Create(NewCanceledContext(), newBook) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Get with cancelled context", func(t *testing.T) { + _, err := repo.Get(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Update with cancelled context", func(t *testing.T) { + book.Title = "Updated" + err := repo.Update(NewCanceledContext(), book) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Delete with cancelled context", func(t *testing.T) { + err := repo.Delete(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("List with cancelled context", func(t *testing.T) { + _, err := repo.List(NewCanceledContext(), BookListOptions{}) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetQueued with cancelled context", func(t *testing.T) { + _, err := repo.GetQueued(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetReading with cancelled context", func(t *testing.T) { + _, err := repo.GetReading(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetFinished with cancelled context", func(t *testing.T) { + _, err := repo.GetFinished(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetByAuthor with cancelled context", func(t *testing.T) { + _, err := repo.GetByAuthor(NewCanceledContext(), "Test Author") + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("StartReading with cancelled context", func(t *testing.T) { + err := repo.StartReading(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("FinishReading with cancelled context", func(t *testing.T) { + err := repo.FinishReading(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("UpdateProgress with cancelled context", func(t *testing.T) { + err := repo.UpdateProgress(NewCanceledContext(), id, 50) + AssertError(t, err, "Expected error with cancelled context") + }) + }) + + t.Run("Edge Cases", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewBookRepository(db) + ctx := context.Background() + + t.Run("Get non-existent book", func(t *testing.T) { + _, err := repo.Get(ctx, 99999) + AssertError(t, err, "Expected error for non-existent book") + }) + + t.Run("Update non-existent book succeeds with no rows affected", func(t *testing.T) { + book := NewBookBuilder().WithTitle("Non-existent").Build() + book.ID = 99999 + err := repo.Update(ctx, book) + AssertNoError(t, err, "Update should not error when no rows affected") + }) + + t.Run("Delete non-existent book succeeds with no rows affected", func(t *testing.T) { + err := repo.Delete(ctx, 99999) + AssertNoError(t, err, "Delete should not error when no rows affected") + }) + + t.Run("StartReading non-existent book", func(t *testing.T) { + err := repo.StartReading(ctx, 99999) + AssertError(t, err, "Expected error for non-existent book") + }) + + t.Run("FinishReading non-existent book", func(t *testing.T) { + err := repo.FinishReading(ctx, 99999) + AssertError(t, err, "Expected error for non-existent book") + }) + + t.Run("UpdateProgress non-existent book", func(t *testing.T) { + err := repo.UpdateProgress(ctx, 99999, 50) + AssertError(t, err, "Expected error for non-existent book") + }) + + t.Run("GetByAuthor with no results", func(t *testing.T) { + books, err := repo.GetByAuthor(ctx, "NonExistentAuthor") + AssertNoError(t, err, "Should not error when no books found") + AssertEqual(t, 0, len(books), "Expected empty result set") }) }) } diff --git a/internal/repo/movie_repository_test.go b/internal/repo/movie_repository_test.go --- a/internal/repo/movie_repository_test.go +++ b/internal/repo/movie_repository_test.go @@ -230,13 +230,94 @@ }) t.Run("Count with context cancellation", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(ctx) - cancel() + _, err := repo.Count(NewCanceledContext(), MovieListOptions{}) + AssertError(t, err, "Expected error with cancelled context") + }) + }) - _, err := repo.Count(cancelCtx, MovieListOptions{}) - if err == nil { - t.Error("Expected error with cancelled context") - } + t.Run("Context Cancellation Error Paths", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewMovieRepository(db) + ctx := context.Background() + + movie := NewMovieBuilder().WithTitle("Test Movie").WithYear(2023).Build() + id, err := repo.Create(ctx, movie) + AssertNoError(t, err, "Failed to create movie") + + t.Run("Create with cancelled context", func(t *testing.T) { + newMovie := NewMovieBuilder().WithTitle("Cancelled").Build() + _, err := repo.Create(NewCanceledContext(), newMovie) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Get with cancelled context", func(t *testing.T) { + _, err := repo.Get(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Update with cancelled context", func(t *testing.T) { + movie.Title = "Updated" + err := repo.Update(NewCanceledContext(), movie) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Delete with cancelled context", func(t *testing.T) { + err := repo.Delete(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("List with cancelled context", func(t *testing.T) { + _, err := repo.List(NewCanceledContext(), MovieListOptions{}) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetQueued with cancelled context", func(t *testing.T) { + _, err := repo.GetQueued(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetWatched with cancelled context", func(t *testing.T) { + _, err := repo.GetWatched(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("MarkWatched with cancelled context", func(t *testing.T) { + err := repo.MarkWatched(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + }) + + t.Run("Edge Cases", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewMovieRepository(db) + ctx := context.Background() + + t.Run("Get non-existent movie", func(t *testing.T) { + _, err := repo.Get(ctx, 99999) + AssertError(t, err, "Expected error for non-existent movie") + }) + + t.Run("Update non-existent movie succeeds with no rows affected", func(t *testing.T) { + movie := NewMovieBuilder().WithTitle("Non-existent").Build() + movie.ID = 99999 + err := repo.Update(ctx, movie) + AssertNoError(t, err, "Update should not error when no rows affected") + }) + + t.Run("Delete non-existent movie succeeds with no rows affected", func(t *testing.T) { + err := repo.Delete(ctx, 99999) + AssertNoError(t, err, "Delete should not error when no rows affected") + }) + + t.Run("MarkWatched non-existent movie", func(t *testing.T) { + err := repo.MarkWatched(ctx, 99999) + AssertError(t, err, "Expected error for non-existent movie") + }) + + t.Run("List with no results", func(t *testing.T) { + movies, err := repo.List(ctx, MovieListOptions{Year: 1900}) + AssertNoError(t, err, "Should not error when no movies found") + AssertEqual(t, 0, len(movies), "Expected empty result set") }) }) } diff --git a/internal/repo/note_repository_test.go b/internal/repo/note_repository_test.go --- a/internal/repo/note_repository_test.go +++ b/internal/repo/note_repository_test.go @@ -9,7 +9,7 @@ ) func TestNoteRepository(t *testing.T) { - t.Run("CRUD", func(t *testing.T) { + t.Run("CRUD Operations", func(t *testing.T) { db := CreateTestDB(t) repo := NewNoteRepository(db) ctx := context.Background() @@ -156,7 +156,7 @@ }) }) - t.Run("Specialized Methods", func(t *testing.T) { + t.Run("Special Methods", func(t *testing.T) { db := CreateTestDB(t) repo := NewNoteRepository(db) ctx := context.Background() @@ -250,24 +250,16 @@ Tags: []string{"initial"}, } id, err := repo.Create(ctx, note) - if err != nil { - t.Fatalf("Failed to create note: %v", err) - } + AssertNoError(t, err, "Failed to create note") t.Run("AddTag", func(t *testing.T) { err := repo.AddTag(ctx, id, "new-tag") - if err != nil { - t.Fatalf("Failed to add tag: %v", err) - } + AssertNoError(t, err, "Failed to add tag") retrieved, err := repo.Get(ctx, id) - if err != nil { - t.Fatalf("Failed to get note: %v", err) - } + AssertNoError(t, err, "Failed to get note") - if len(retrieved.Tags) != 2 { - t.Errorf("Expected 2 tags, got %d", len(retrieved.Tags)) - } + AssertEqual(t, 2, len(retrieved.Tags), "Expected 2 tags") found := false for _, tag := range retrieved.Tags { @@ -276,9 +268,7 @@ break } } - if !found { - t.Error("New tag not found in note") - } + AssertTrue(t, found, "New tag not found in note") }) t.Run("AddTag Duplicate", func(t *testing.T) { @@ -323,91 +313,110 @@ } _, err := repo.Create(ctx, note1) - if err != nil { - t.Fatalf("Failed to create note1: %v", err) - } + AssertNoError(t, err, "Failed to create note1") _, err = repo.Create(ctx, note2) - if err != nil { - t.Fatalf("Failed to create note2: %v", err) - } + AssertNoError(t, err, "Failed to create note2") _, err = repo.Create(ctx, note3) - if err != nil { - t.Fatalf("Failed to create note3: %v", err) - } + AssertNoError(t, err, "Failed to create note3") results, err := repo.GetByTags(ctx, []string{"work"}) - if err != nil { - t.Fatalf("Failed to get notes by tag: %v", err) - } - - if len(results) < 2 { - t.Errorf("Expected at least 2 notes with 'work' tag, got %d", len(results)) - } + AssertNoError(t, err, "Failed to get notes by tag") + AssertTrue(t, len(results) >= 2, "Expected at least 2 notes with 'work' tag") results, err = repo.GetByTags(ctx, []string{"nonexistent"}) - if err != nil { - t.Fatalf("Failed to get notes by nonexistent tag: %v", err) - } - - if len(results) != 0 { - t.Errorf("Expected 0 notes with nonexistent tag, got %d", len(results)) - } + AssertNoError(t, err, "Failed to get notes by nonexistent tag") + AssertEqual(t, 0, len(results), "Expected 0 notes with nonexistent tag") results, err = repo.GetByTags(ctx, []string{}) - if err != nil { - t.Fatalf("Failed to get notes with empty tags: %v", err) - } - - if len(results) != 0 { - t.Errorf("Expected 0 notes with empty tag list, got %d", len(results)) - } + AssertNoError(t, err, "Failed to get notes with empty tags") + AssertEqual(t, 0, len(results), "Expected 0 notes with empty tag list") }) }) - t.Run("Error Cases", func(t *testing.T) { + t.Run("Context Cancellation Error Paths", func(t *testing.T) { db := CreateTestDB(t) repo := NewNoteRepository(db) ctx := context.Background() - t.Run("Get Nonexistent Note", func(t *testing.T) { - _, err := repo.Get(ctx, 999) - if err == nil { - t.Error("Expected error when getting nonexistent note") - } + note := NewNoteBuilder().WithTitle("Test Note").WithContent("Test content").Build() + id, err := repo.Create(ctx, note) + AssertNoError(t, err, "Failed to create note") + + t.Run("Create with cancelled context", func(t *testing.T) { + newNote := NewNoteBuilder().WithTitle("Cancelled").Build() + _, err := repo.Create(NewCanceledContext(), newNote) + AssertError(t, err, "Expected error with cancelled context") }) - t.Run("Update Nonexistent Note", func(t *testing.T) { - note := &models.Note{ - ID: 999, - Title: "Nonexistent", - Content: "Should fail", - } - - err := repo.Update(ctx, note) - if err == nil { - t.Error("Expected error when updating nonexistent note") - } + t.Run("Get with cancelled context", func(t *testing.T) { + _, err := repo.Get(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") }) - t.Run("Delete Nonexistent Note", func(t *testing.T) { - err := repo.Delete(ctx, 999) - if err == nil { - t.Error("Expected error when deleting nonexistent note") - } + t.Run("Update with cancelled context", func(t *testing.T) { + note.Title = "Updated" + err := repo.Update(NewCanceledContext(), note) + AssertError(t, err, "Expected error with cancelled context") }) - t.Run("Archive Nonexistent Note", func(t *testing.T) { - err := repo.Archive(ctx, 999) - if err == nil { - t.Error("Expected error when archiving nonexistent note") - } + t.Run("Delete with cancelled context", func(t *testing.T) { + err := repo.Delete(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") }) - t.Run("AddTag to Nonexistent Note", func(t *testing.T) { - err := repo.AddTag(ctx, 999, "tag") - if err == nil { - t.Error("Expected error when adding tag to nonexistent note") - } + t.Run("List with cancelled context", func(t *testing.T) { + _, err := repo.List(NewCanceledContext(), NoteListOptions{}) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetByTitle with cancelled context", func(t *testing.T) { + _, err := repo.GetByTitle(NewCanceledContext(), "Test") + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetArchived with cancelled context", func(t *testing.T) { + _, err := repo.GetArchived(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetActive with cancelled context", func(t *testing.T) { + _, err := repo.GetActive(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Archive with cancelled context", func(t *testing.T) { + err := repo.Archive(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Unarchive with cancelled context", func(t *testing.T) { + err := repo.Unarchive(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("SearchContent with cancelled context", func(t *testing.T) { + _, err := repo.SearchContent(NewCanceledContext(), "test") + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetRecent with cancelled context", func(t *testing.T) { + _, err := repo.GetRecent(NewCanceledContext(), 10) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("AddTag with cancelled context", func(t *testing.T) { + err := repo.AddTag(NewCanceledContext(), id, "tag") + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("RemoveTag with cancelled context", func(t *testing.T) { + err := repo.RemoveTag(NewCanceledContext(), id, "tag") + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetByTags with cancelled context", func(t *testing.T) { + _, err := repo.GetByTags(NewCanceledContext(), []string{"tag"}) + AssertError(t, err, "Expected error with cancelled context") }) }) @@ -416,7 +425,38 @@ repo := NewNoteRepository(db) ctx := context.Background() - t.Run("Note with Empty Tags", func(t *testing.T) { + t.Run("Get non-existent note", func(t *testing.T) { + _, err := repo.Get(ctx, 99999) + AssertError(t, err, "Expected error for non-existent note") + }) + + t.Run("Update non-existent note", func(t *testing.T) { + note := &models.Note{ + ID: 99999, + Title: "Nonexistent", + Content: "Should fail", + } + + err := repo.Update(ctx, note) + AssertError(t, err, "Expected error when updating non-existent note") + }) + + t.Run("Delete non-existent note", func(t *testing.T) { + err := repo.Delete(ctx, 99999) + AssertError(t, err, "Expected error when deleting non-existent note") + }) + + t.Run("Archive non-existent note", func(t *testing.T) { + err := repo.Archive(ctx, 99999) + AssertError(t, err, "Expected error when archiving non-existent note") + }) + + t.Run("AddTag to non-existent note", func(t *testing.T) { + err := repo.AddTag(ctx, 99999, "tag") + AssertError(t, err, "Expected error when adding tag to non-existent note") + }) + + t.Run("Note with empty tags", func(t *testing.T) { note := &models.Note{ Title: "No Tags Note", Content: "This note has no tags", @@ -424,21 +464,15 @@ } id, err := repo.Create(ctx, note) - if err != nil { - t.Fatalf("Failed to create note with empty tags: %v", err) - } + AssertNoError(t, err, "Failed to create note with empty tags") retrieved, err := repo.Get(ctx, id) - if err != nil { - t.Fatalf("Failed to get note: %v", err) - } + AssertNoError(t, err, "Failed to get note") - if len(retrieved.Tags) != 0 { - t.Errorf("Expected empty tags slice, got %d tags", len(retrieved.Tags)) - } + AssertEqual(t, 0, len(retrieved.Tags), "Expected empty tags slice") }) - t.Run("Note with Nil Tags", func(t *testing.T) { + t.Run("Note with nil tags", func(t *testing.T) { note := &models.Note{ Title: "Nil Tags Note", Content: "This note has nil tags", @@ -446,21 +480,15 @@ } id, err := repo.Create(ctx, note) - if err != nil { - t.Fatalf("Failed to create note with nil tags: %v", err) - } + AssertNoError(t, err, "Failed to create note with nil tags") retrieved, err := repo.Get(ctx, id) - if err != nil { - t.Fatalf("Failed to get note: %v", err) - } + AssertNoError(t, err, "Failed to get note") - if retrieved.Tags != nil { - t.Errorf("Expected nil tags, got %v", retrieved.Tags) - } + AssertEqual(t, 0, len(retrieved.Tags), "Expected empty tags") }) - t.Run("Note with Long Content", func(t *testing.T) { + t.Run("Note with long content", func(t *testing.T) { longContent := "" for i := 0; i < 1000; i++ { longContent += "This is a very long content string. " @@ -472,18 +500,18 @@ } id, err := repo.Create(ctx, note) - if err != nil { - t.Fatalf("Failed to create note with long content: %v", err) - } + AssertNoError(t, err, "Failed to create note with long content") retrieved, err := repo.Get(ctx, id) - if err != nil { - t.Fatalf("Failed to get note: %v", err) - } + AssertNoError(t, err, "Failed to get note") - if retrieved.Content != longContent { - t.Error("Long content was not stored/retrieved correctly") - } + AssertEqual(t, longContent, retrieved.Content, "Long content was not stored/retrieved correctly") + }) + + t.Run("List with no results", func(t *testing.T) { + notes, err := repo.List(ctx, NoteListOptions{Title: "NonexistentTitle"}) + AssertNoError(t, err, "Should not error when no notes found") + AssertEqual(t, 0, len(notes), "Expected empty result set") }) }) } diff --git a/internal/repo/test_utilities.go b/internal/repo/test_utilities.go --- a/internal/repo/test_utilities.go +++ b/internal/repo/test_utilities.go @@ -340,6 +340,221 @@ return b.task } +// BookBuilder provides a fluent interface for building test books +type BookBuilder struct { + book *models.Book +} + +// NewBookBuilder creates a new BookBuilder with sensible defaults +func NewBookBuilder() *BookBuilder { + return &BookBuilder{ + book: &models.Book{ + Status: "queued", + Progress: 0, + Added: time.Now(), + }, + } +} + +func (b *BookBuilder) WithTitle(title string) *BookBuilder { + b.book.Title = title + return b +} + +func (b *BookBuilder) WithAuthor(author string) *BookBuilder { + b.book.Author = author + return b +} + +func (b *BookBuilder) WithStatus(status string) *BookBuilder { + b.book.Status = status + return b +} + +func (b *BookBuilder) WithProgress(progress int) *BookBuilder { + b.book.Progress = progress + return b +} + +func (b *BookBuilder) WithPages(pages int) *BookBuilder { + b.book.Pages = pages + return b +} + +func (b *BookBuilder) WithRating(rating float64) *BookBuilder { + b.book.Rating = rating + return b +} + +func (b *BookBuilder) WithNotes(notes string) *BookBuilder { + b.book.Notes = notes + return b +} + +func (b *BookBuilder) WithStarted(started time.Time) *BookBuilder { + b.book.Started = &started + return b +} + +func (b *BookBuilder) WithFinished(finished time.Time) *BookBuilder { + b.book.Finished = &finished + return b +} + +func (b *BookBuilder) Build() *models.Book { + return b.book +} + +// MovieBuilder provides a fluent interface for building test movies +type MovieBuilder struct { + movie *models.Movie +} + +// NewMovieBuilder creates a new MovieBuilder with sensible defaults +func NewMovieBuilder() *MovieBuilder { + return &MovieBuilder{ + movie: &models.Movie{ + Status: "queued", + Added: time.Now(), + }, + } +} + +func (b *MovieBuilder) WithTitle(title string) *MovieBuilder { + b.movie.Title = title + return b +} + +func (b *MovieBuilder) WithYear(year int) *MovieBuilder { + b.movie.Year = year + return b +} + +func (b *MovieBuilder) WithStatus(status string) *MovieBuilder { + b.movie.Status = status + return b +} + +func (b *MovieBuilder) WithRating(rating float64) *MovieBuilder { + b.movie.Rating = rating + return b +} + +func (b *MovieBuilder) WithNotes(notes string) *MovieBuilder { + b.movie.Notes = notes + return b +} + +func (b *MovieBuilder) WithWatched(watched time.Time) *MovieBuilder { + b.movie.Watched = &watched + return b +} + +func (b *MovieBuilder) Build() *models.Movie { + return b.movie +} + +// TVShowBuilder provides a fluent interface for building test TV shows +type TVShowBuilder struct { + show *models.TVShow +} + +// NewTVShowBuilder creates a new TVShowBuilder with sensible defaults +func NewTVShowBuilder() *TVShowBuilder { + return &TVShowBuilder{ + show: &models.TVShow{ + Status: "queued", + Season: 1, + Episode: 1, + Added: time.Now(), + }, + } +} + +func (b *TVShowBuilder) WithTitle(title string) *TVShowBuilder { + b.show.Title = title + return b +} + +func (b *TVShowBuilder) WithSeason(season int) *TVShowBuilder { + b.show.Season = season + return b +} + +func (b *TVShowBuilder) WithEpisode(episode int) *TVShowBuilder { + b.show.Episode = episode + return b +} + +func (b *TVShowBuilder) WithStatus(status string) *TVShowBuilder { + b.show.Status = status + return b +} + +func (b *TVShowBuilder) WithRating(rating float64) *TVShowBuilder { + b.show.Rating = rating + return b +} + +func (b *TVShowBuilder) WithNotes(notes string) *TVShowBuilder { + b.show.Notes = notes + return b +} + +func (b *TVShowBuilder) WithLastWatched(lastWatched time.Time) *TVShowBuilder { + b.show.LastWatched = &lastWatched + return b +} + +func (b *TVShowBuilder) Build() *models.TVShow { + return b.show +} + +// NoteBuilder provides a fluent interface for building test notes +type NoteBuilder struct { + note *models.Note +} + +// NewNoteBuilder creates a new NoteBuilder with sensible defaults +func NewNoteBuilder() *NoteBuilder { + return &NoteBuilder{ + note: &models.Note{ + Archived: false, + Created: time.Now(), + Modified: time.Now(), + }, + } +} + +func (b *NoteBuilder) WithTitle(title string) *NoteBuilder { + b.note.Title = title + return b +} + +func (b *NoteBuilder) WithContent(content string) *NoteBuilder { + b.note.Content = content + return b +} + +func (b *NoteBuilder) WithTags(tags []string) *NoteBuilder { + b.note.Tags = tags + return b +} + +func (b *NoteBuilder) WithArchived(archived bool) *NoteBuilder { + b.note.Archived = archived + return b +} + +func (b *NoteBuilder) WithFilePath(filePath string) *NoteBuilder { + b.note.FilePath = filePath + return b +} + +func (b *NoteBuilder) Build() *models.Note { + return b.note +} + // SetupTestData creates sample data in the database and returns the repositories func SetupTestData(t *testing.T, db *sql.DB) *Repositories { ctx := context.Background() diff --git a/internal/repo/time_entry_repository_test.go b/internal/repo/time_entry_repository_test.go --- a/internal/repo/time_entry_repository_test.go +++ b/internal/repo/time_entry_repository_test.go @@ -4,35 +4,17 @@ "context" "database/sql" "fmt" - "os" "testing" "time" + _ "github.com/mattn/go-sqlite3" "github.com/stormlightlabs/noteleaf/internal/models" - "github.com/stormlightlabs/noteleaf/internal/store" ) -func setupTimeEntryTestDB(t *testing.T) (*sql.DB, *TimeEntryRepository, *TaskRepository, func()) { - os.Setenv("NOTELEAF_CONFIG_DIR", t.TempDir()) - - db, err := store.NewDatabase() - if err != nil { - t.Fatalf("Failed to create test database: %v", err) - } - - timeRepo := NewTimeEntryRepository(db.DB) - taskRepo := NewTaskRepository(db.DB) - - cleanup := func() { - db.Close() - os.Unsetenv("NOTELEAF_CONFIG_DIR") - } - - return db.DB, timeRepo, taskRepo, cleanup -} - -func createTestTask(t *testing.T, taskRepo *TaskRepository) *models.Task { +func createTestTask(t *testing.T, db *sql.DB) *models.Task { + t.Helper() ctx := context.Background() + taskRepo := NewTaskRepository(db) task := &models.Task{ UUID: fmt.Sprintf("test-uuid-%d", time.Now().UnixNano()), Description: "Test Task", @@ -40,365 +22,243 @@ } id, err := taskRepo.Create(ctx, task) - if err != nil { - t.Fatalf("Failed to create test task: %v", err) - } + AssertNoError(t, err, "Failed to create test task") task.ID = id return task } func TestTimeEntryRepository(t *testing.T) { - t.Run("Start", func(t *testing.T) { - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) - defer cleanup() - + t.Run("CRUD Operations", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) ctx := context.Background() - task := createTestTask(t, taskRepo) + task := createTestTask(t, db) - t.Run("starts time tracking successfully", func(t *testing.T) { + t.Run("Start time tracking", func(t *testing.T) { description := "Working on feature" entry, err := repo.Start(ctx, task.ID, description) - if err != nil { - t.Fatalf("Failed to start time tracking: %v", err) - } - - if entry.ID == 0 { - t.Error("Expected entry to have an ID") - } - if entry.TaskID != task.ID { - t.Errorf("Expected TaskID %d, got %d", task.ID, entry.TaskID) - } - if entry.Description != description { - t.Errorf("Expected description %q, got %q", description, entry.Description) - } - if entry.EndTime != nil { - t.Error("Expected EndTime to be nil for active entry") - } - if !entry.IsActive() { - t.Error("Expected entry to be active") - } + AssertNoError(t, err, "Failed to start time tracking") + AssertNotEqual(t, int64(0), entry.ID, "Expected non-zero entry ID") + AssertEqual(t, task.ID, entry.TaskID, "Expected TaskID to match") + AssertEqual(t, description, entry.Description, "Expected description to match") + AssertTrue(t, entry.EndTime == nil, "Expected EndTime to be nil for active entry") + AssertTrue(t, entry.IsActive(), "Expected entry to be active") }) - t.Run("prevents starting already active task", func(t *testing.T) { + t.Run("Prevent starting already active task", func(t *testing.T) { _, err := repo.Start(ctx, task.ID, "Another attempt") - if err == nil { - t.Error("Expected error when starting already active task") - } - if err.Error() != "task already has an active time entry" { - t.Errorf("Expected specific error message, got: %v", err) - } + AssertError(t, err, "Expected error when starting already active task") + AssertContains(t, err.Error(), "task already has an active time entry", "Expected specific error message") }) - }) - t.Run("Stop", func(t *testing.T) { - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) - defer cleanup() + t.Run("Stop active time entry", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) - ctx := context.Background() - task := createTestTask(t, taskRepo) + entry, err := repo.Start(ctx, task.ID, "Test work") + AssertNoError(t, err, "Failed to start time tracking") - entry, err := repo.Start(ctx, task.ID, "Test work") - if err != nil { - t.Fatalf("Failed to start time tracking: %v", err) - } + time.Sleep(1010 * time.Millisecond) - time.Sleep(1010 * time.Millisecond) - - t.Run("stops active time entry", func(t *testing.T) { stoppedEntry, err := repo.Stop(ctx, entry.ID) - - if err != nil { - t.Fatalf("Failed to stop time tracking: %v", err) - } - - if stoppedEntry.EndTime == nil { - t.Error("Expected EndTime to be set") - } - if stoppedEntry.DurationSeconds <= 0 { - t.Error("Expected duration to be greater than 0") - } - if stoppedEntry.IsActive() { - t.Error("Expected entry to not be active after stopping") - } + AssertNoError(t, err, "Failed to stop time tracking") + AssertTrue(t, stoppedEntry.EndTime != nil, "Expected EndTime to be set") + AssertGreaterThan(t, stoppedEntry.DurationSeconds, int64(0), "Expected duration > 0") + AssertFalse(t, stoppedEntry.IsActive(), "Expected entry to not be active after stopping") }) - t.Run("fails to stop already stopped entry", func(t *testing.T) { - _, err := repo.Stop(ctx, entry.ID) + t.Run("Fail to stop already stopped entry", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) - if err == nil { - t.Error("Expected error when stopping already stopped entry") - } - if err.Error() != "time entry is not active" { - t.Errorf("Expected specific error message, got: %v", err) - } - }) - }) + entry, err := repo.Start(ctx, task.ID, "Test work") + AssertNoError(t, err, "Failed to start time tracking") - t.Run("StopActiveByTaskID", func(t *testing.T) { - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) - defer cleanup() + time.Sleep(1010 * time.Millisecond) + _, err = repo.Stop(ctx, entry.ID) + AssertNoError(t, err, "Failed to stop time tracking") - ctx := context.Background() - task := createTestTask(t, taskRepo) - - t.Run("stops active entry by task ID", func(t *testing.T) { - _, err := repo.Start(ctx, task.ID, "Test work") - if err != nil { - t.Fatalf("Failed to start time tracking: %v", err) - } - - stoppedEntry, err := repo.StopActiveByTaskID(ctx, task.ID) - - if err != nil { - t.Fatalf("Failed to stop time tracking by task ID: %v", err) - } - - if stoppedEntry.EndTime == nil { - t.Error("Expected EndTime to be set") - } - if stoppedEntry.IsActive() { - t.Error("Expected entry to not be active") - } + _, err = repo.Stop(ctx, entry.ID) + AssertError(t, err, "Expected error when stopping already stopped entry") + AssertContains(t, err.Error(), "time entry is not active", "Expected specific error message") }) - t.Run("fails when no active entry exists", func(t *testing.T) { - _, err := repo.StopActiveByTaskID(ctx, task.ID) + t.Run("Get time entry", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) - if err == nil { - t.Error("Expected error when no active entry exists") - } - if err.Error() != "no active time entry found for task" { - t.Errorf("Expected specific error message, got: %v", err) - } + original, err := repo.Start(ctx, task.ID, "Test entry") + AssertNoError(t, err, "Failed to start time tracking") + + retrieved, err := repo.Get(ctx, original.ID) + AssertNoError(t, err, "Failed to get time entry") + AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") + AssertEqual(t, original.TaskID, retrieved.TaskID, "TaskID mismatch") + AssertEqual(t, original.Description, retrieved.Description, "Description mismatch") + }) + + t.Run("Delete time entry", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) + + entry, err := repo.Start(ctx, task.ID, "To be deleted") + AssertNoError(t, err, "Failed to create entry") + + err = repo.Delete(ctx, entry.ID) + AssertNoError(t, err, "Failed to delete entry") + + _, err = repo.Get(ctx, entry.ID) + AssertError(t, err, "Expected error when getting deleted entry") + AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") }) }) - t.Run("GetActiveByTaskID", func(t *testing.T) { - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) - defer cleanup() - + t.Run("Query Methods", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) ctx := context.Background() - task := createTestTask(t, taskRepo) + task := createTestTask(t, db) - t.Run("returns nil when no active entry exists", func(t *testing.T) { + t.Run("GetActiveByTaskID returns error when no active entry", func(t *testing.T) { _, err := repo.GetActiveByTaskID(ctx, task.ID) - - if err != sql.ErrNoRows { - t.Errorf("Expected sql.ErrNoRows, got: %v", err) - } + AssertError(t, err, "Expected error when no active entry exists") + AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") }) - t.Run("returns active entry when one exists", func(t *testing.T) { + t.Run("GetActiveByTaskID returns active entry", func(t *testing.T) { startedEntry, err := repo.Start(ctx, task.ID, "Test work") - if err != nil { - t.Fatalf("Failed to start time tracking: %v", err) - } + AssertNoError(t, err, "Failed to start time tracking") activeEntry, err := repo.GetActiveByTaskID(ctx, task.ID) - - if err != nil { - t.Fatalf("Failed to get active entry: %v", err) - } - - if activeEntry.ID != startedEntry.ID { - t.Errorf("Expected entry ID %d, got %d", startedEntry.ID, activeEntry.ID) - } - if !activeEntry.IsActive() { - t.Error("Expected entry to be active") - } + AssertNoError(t, err, "Failed to get active entry") + AssertEqual(t, startedEntry.ID, activeEntry.ID, "Expected entry IDs to match") + AssertTrue(t, activeEntry.IsActive(), "Expected entry to be active") }) - }) - t.Run("GetByTaskID", func(t *testing.T) { - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) - defer cleanup() + t.Run("StopActiveByTaskID stops active entry", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) - ctx := context.Background() - task := createTestTask(t, taskRepo) + _, err := repo.Start(ctx, task.ID, "Test work") + AssertNoError(t, err, "Failed to start time tracking") - t.Run("returns empty slice when no entries exist", func(t *testing.T) { + stoppedEntry, err := repo.StopActiveByTaskID(ctx, task.ID) + AssertNoError(t, err, "Failed to stop time tracking by task ID") + AssertTrue(t, stoppedEntry.EndTime != nil, "Expected EndTime to be set") + AssertFalse(t, stoppedEntry.IsActive(), "Expected entry to not be active") + }) + + t.Run("StopActiveByTaskID fails when no active entry", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) + + _, err := repo.StopActiveByTaskID(ctx, task.ID) + AssertError(t, err, "Expected error when no active entry exists") + AssertContains(t, err.Error(), "no active time entry found for task", "Expected specific error message") + }) + + t.Run("GetByTaskID returns empty when no entries", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) + entries, err := repo.GetByTaskID(ctx, task.ID) - - if err != nil { - t.Fatalf("Failed to get entries: %v", err) - } - - if len(entries) != 0 { - t.Errorf("Expected 0 entries, got %d", len(entries)) - } + AssertNoError(t, err, "Failed to get entries") + AssertEqual(t, 0, len(entries), "Expected 0 entries") }) - t.Run("returns all entries for task", func(t *testing.T) { + t.Run("GetByTaskID returns all entries for task", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) + _, err := repo.Start(ctx, task.ID, "First session") - if err != nil { - t.Fatalf("Failed to start first session: %v", err) - } + AssertNoError(t, err, "Failed to start first session") _, err = repo.StopActiveByTaskID(ctx, task.ID) - if err != nil { - t.Fatalf("Failed to stop first session: %v", err) - } + AssertNoError(t, err, "Failed to stop first session") _, err = repo.Start(ctx, task.ID, "Second session") - if err != nil { - t.Fatalf("Failed to start second session: %v", err) - } + AssertNoError(t, err, "Failed to start second session") entries, err := repo.GetByTaskID(ctx, task.ID) - - if err != nil { - t.Fatalf("Failed to get entries: %v", err) - } - - if len(entries) != 2 { - t.Errorf("Expected 2 entries, got %d", len(entries)) - } - - if entries[0].Description != "Second session" { - t.Errorf("Expected first entry to be 'Second session', got %q", entries[0].Description) - } - if entries[1].Description != "First session" { - t.Errorf("Expected second entry to be 'First session', got %q", entries[1].Description) - } + AssertNoError(t, err, "Failed to get entries") + AssertEqual(t, 2, len(entries), "Expected 2 entries") + AssertEqual(t, "Second session", entries[0].Description, "Expected newest entry first") + AssertEqual(t, "First session", entries[1].Description, "Expected oldest entry second") }) - }) - t.Run("GetTotalTimeByTaskID", func(t *testing.T) { - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) - defer cleanup() + t.Run("GetTotalTimeByTaskID returns zero when no entries", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) - ctx := context.Background() - task := createTestTask(t, taskRepo) - - t.Run("returns zero duration when no entries exist", func(t *testing.T) { duration, err := repo.GetTotalTimeByTaskID(ctx, task.ID) - - if err != nil { - t.Fatalf("Failed to get total time: %v", err) - } - - if duration != 0 { - t.Errorf("Expected 0 duration, got %v", duration) - } + AssertNoError(t, err, "Failed to get total time") + AssertEqual(t, time.Duration(0), duration, "Expected 0 duration") }) - t.Run("calculates total time including active entries", func(t *testing.T) { + t.Run("GetTotalTimeByTaskID calculates total including active entries", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) + entry1, err := repo.Start(ctx, task.ID, "Completed work") - if err != nil { - t.Fatalf("Failed to start first entry: %v", err) - } + AssertNoError(t, err, "Failed to start first entry") time.Sleep(1010 * time.Millisecond) _, err = repo.Stop(ctx, entry1.ID) - if err != nil { - t.Fatalf("Failed to stop first entry: %v", err) - } + AssertNoError(t, err, "Failed to stop first entry") _, err = repo.Start(ctx, task.ID, "Active work") - if err != nil { - t.Fatalf("Failed to start second entry: %v", err) - } + AssertNoError(t, err, "Failed to start second entry") time.Sleep(1010 * time.Millisecond) totalTime, err := repo.GetTotalTimeByTaskID(ctx, task.ID) - - if err != nil { - t.Fatalf("Failed to get total time: %v", err) - } - - if totalTime <= 0 { - t.Error("Expected total time to be greater than 0") - } - - if totalTime < 2*time.Second { - t.Errorf("Expected total time to be at least 2s, got %v", totalTime) - } - }) - }) - - t.Run("Delete", func(t *testing.T) { - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) - defer cleanup() - - ctx := context.Background() - task := createTestTask(t, taskRepo) - - t.Run("deletes existing entry", func(t *testing.T) { - entry, err := repo.Start(ctx, task.ID, "To be deleted") - if err != nil { - t.Fatalf("Failed to create entry: %v", err) - } - - err = repo.Delete(ctx, entry.ID) - - if err != nil { - t.Fatalf("Failed to delete entry: %v", err) - } - - _, err = repo.Get(ctx, entry.ID) - if err != sql.ErrNoRows { - t.Errorf("Expected entry to be deleted, but got: %v", err) - } - }) - - t.Run("fails to delete non-existent entry", func(t *testing.T) { - err := repo.Delete(ctx, 99999) - - if err == nil { - t.Error("Expected error when deleting non-existent entry") - } - if err.Error() != "time entry not found" { - t.Errorf("Expected specific error message, got: %v", err) - } + AssertNoError(t, err, "Failed to get total time") + AssertTrue(t, totalTime > 0, "Expected total time > 0") + AssertTrue(t, totalTime >= 2*time.Second, "Expected total time >= 2s") }) }) t.Run("GetByDateRange", func(t *testing.T) { - _, repo, taskRepo, cleanup := setupTimeEntryTestDB(t) - defer cleanup() - + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) ctx := context.Background() - t.Run("returns empty slice when no entries in range", func(t *testing.T) { + t.Run("Returns empty when no entries in range", func(t *testing.T) { start := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) end := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC) entries, err := repo.GetByDateRange(ctx, start, end) - - if err != nil { - t.Fatalf("Failed to get entries by date range: %v", err) - } - - if len(entries) != 0 { - t.Errorf("Expected 0 entries, got %d", len(entries)) - } + AssertNoError(t, err, "Failed to get entries by date range") + AssertEqual(t, 0, len(entries), "Expected 0 entries") }) - t.Run("returns entries within date range", func(t *testing.T) { - task := createTestTask(t, taskRepo) + t.Run("Returns entries within date range", func(t *testing.T) { + task := createTestTask(t, db) entry, err := repo.Start(ctx, task.ID, "Test entry") - if err != nil { - t.Fatalf("Failed to start entry: %v", err) - } + AssertNoError(t, err, "Failed to start entry") _, err = repo.Stop(ctx, entry.ID) - if err != nil { - t.Fatalf("Failed to stop entry: %v", err) - } + AssertNoError(t, err, "Failed to stop entry") now := time.Now() start := now.Add(-time.Hour) end := now.Add(time.Hour) entries, err := repo.GetByDateRange(ctx, start, end) - - if err != nil { - t.Fatalf("Failed to get entries by date range: %v", err) - } + AssertNoError(t, err, "Failed to get entries by date range") found := false for _, e := range entries { @@ -407,33 +267,23 @@ break } } - - if !found { - t.Error("Expected to find 'Test entry' in results") - } + AssertTrue(t, found, "Expected to find 'Test entry' in results") }) - t.Run("respects date range boundaries", func(t *testing.T) { - task := createTestTask(t, taskRepo) + t.Run("Respects date range boundaries", func(t *testing.T) { + task := createTestTask(t, db) entry, err := repo.Start(ctx, task.ID, "Boundary test") - if err != nil { - t.Fatalf("Failed to start entry: %v", err) - } + AssertNoError(t, err, "Failed to start entry") _, err = repo.Stop(ctx, entry.ID) - if err != nil { - t.Fatalf("Failed to stop entry: %v", err) - } + AssertNoError(t, err, "Failed to stop entry") start := time.Now().Add(time.Hour) end := time.Now().Add(2 * time.Hour) entries, err := repo.GetByDateRange(ctx, start, end) - - if err != nil { - t.Fatalf("Failed to get entries by date range: %v", err) - } + AssertNoError(t, err, "Failed to get entries by date range") for _, e := range entries { if e.Description == "Boundary test" { @@ -442,32 +292,117 @@ } }) - t.Run("handles context cancellation", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(ctx) - cancel() - - start := time.Now().AddDate(0, 0, -1) - end := time.Now() - - _, err := repo.GetByDateRange(cancelCtx, start, end) - if err == nil { - t.Error("Expected error with cancelled context") - } - }) - - t.Run("handles invalid date range", func(t *testing.T) { + t.Run("Handles invalid date range", func(t *testing.T) { start := time.Now() end := time.Now().AddDate(0, 0, -1) entries, err := repo.GetByDateRange(ctx, start, end) + AssertNoError(t, err, "Should not error with invalid date range") + AssertEqual(t, 0, len(entries), "Expected 0 entries with invalid range") + }) + }) - if err != nil { - t.Fatalf("Unexpected error with invalid date range: %v", err) - } + t.Run("Context Cancellation Error Paths", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + ctx := context.Background() + task := createTestTask(t, db) - if len(entries) != 0 { - t.Errorf("Expected 0 entries with invalid range, got %d", len(entries)) - } + entry, err := repo.Start(ctx, task.ID, "Test entry") + AssertNoError(t, err, "Failed to create entry") + + t.Run("Start with cancelled context", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + task := createTestTask(t, db) + + _, err := repo.Start(NewCanceledContext(), task.ID, "Cancelled") + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Get with cancelled context", func(t *testing.T) { + _, err := repo.Get(NewCanceledContext(), entry.ID) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Stop with cancelled context", func(t *testing.T) { + _, err := repo.Stop(NewCanceledContext(), entry.ID) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetActiveByTaskID with cancelled context", func(t *testing.T) { + _, err := repo.GetActiveByTaskID(NewCanceledContext(), task.ID) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("StopActiveByTaskID with cancelled context", func(t *testing.T) { + _, err := repo.StopActiveByTaskID(NewCanceledContext(), task.ID) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetByTaskID with cancelled context", func(t *testing.T) { + _, err := repo.GetByTaskID(NewCanceledContext(), task.ID) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetTotalTimeByTaskID with cancelled context", func(t *testing.T) { + _, err := repo.GetTotalTimeByTaskID(NewCanceledContext(), task.ID) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Delete with cancelled context", func(t *testing.T) { + err := repo.Delete(NewCanceledContext(), entry.ID) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetByDateRange with cancelled context", func(t *testing.T) { + start := time.Now().AddDate(0, 0, -1) + end := time.Now() + + _, err := repo.GetByDateRange(NewCanceledContext(), start, end) + AssertError(t, err, "Expected error with cancelled context") + }) + }) + + t.Run("Edge Cases", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTimeEntryRepository(db) + ctx := context.Background() + + t.Run("Get non-existent entry", func(t *testing.T) { + _, err := repo.Get(ctx, 99999) + AssertError(t, err, "Expected error for non-existent entry") + AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") + }) + + t.Run("Stop non-existent entry", func(t *testing.T) { + _, err := repo.Stop(ctx, 99999) + AssertError(t, err, "Expected error for non-existent entry") + }) + + t.Run("Delete non-existent entry", func(t *testing.T) { + err := repo.Delete(ctx, 99999) + AssertError(t, err, "Expected error for non-existent entry") + AssertContains(t, err.Error(), "time entry not found", "Expected specific error message") + }) + + t.Run("Start with non-existent task", func(t *testing.T) { + _, err := repo.Start(ctx, 99999, "Test") + AssertError(t, err, "Expected error for non-existent task") + }) + + t.Run("GetActiveByTaskID with no results", func(t *testing.T) { + task := createTestTask(t, db) + _, err := repo.GetActiveByTaskID(ctx, task.ID) + AssertError(t, err, "Expected error when no active entry") + AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") + }) + + t.Run("GetByTaskID with no results", func(t *testing.T) { + task := createTestTask(t, db) + entries, err := repo.GetByTaskID(ctx, task.ID) + AssertNoError(t, err, "Should not error when no entries found") + AssertEqual(t, 0, len(entries), "Expected empty result set") }) }) } diff --git a/internal/repo/tv_repository_test.go b/internal/repo/tv_repository_test.go --- a/internal/repo/tv_repository_test.go +++ b/internal/repo/tv_repository_test.go @@ -463,13 +463,125 @@ }) t.Run("Count with context cancellation", func(t *testing.T) { - cancelCtx, cancel := context.WithCancel(ctx) - cancel() + _, err := repo.Count(NewCanceledContext(), TVListOptions{}) + AssertError(t, err, "Expected error with cancelled context") + }) + }) - _, err := repo.Count(cancelCtx, TVListOptions{}) - if err == nil { - t.Error("Expected error with cancelled context") - } + t.Run("Context Cancellation Error Paths", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTVRepository(db) + ctx := context.Background() + + tvShow := NewTVShowBuilder().WithTitle("Test Show").WithSeason(1).WithEpisode(1).Build() + id, err := repo.Create(ctx, tvShow) + AssertNoError(t, err, "Failed to create TV show") + + t.Run("Create with cancelled context", func(t *testing.T) { + newShow := NewTVShowBuilder().WithTitle("Cancelled").Build() + _, err := repo.Create(NewCanceledContext(), newShow) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Get with cancelled context", func(t *testing.T) { + _, err := repo.Get(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Update with cancelled context", func(t *testing.T) { + tvShow.Title = "Updated" + err := repo.Update(NewCanceledContext(), tvShow) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("Delete with cancelled context", func(t *testing.T) { + err := repo.Delete(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("List with cancelled context", func(t *testing.T) { + _, err := repo.List(NewCanceledContext(), TVListOptions{}) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetQueued with cancelled context", func(t *testing.T) { + _, err := repo.GetQueued(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetWatching with cancelled context", func(t *testing.T) { + _, err := repo.GetWatching(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetWatched with cancelled context", func(t *testing.T) { + _, err := repo.GetWatched(NewCanceledContext()) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetByTitle with cancelled context", func(t *testing.T) { + _, err := repo.GetByTitle(NewCanceledContext(), "Test Show") + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("GetBySeason with cancelled context", func(t *testing.T) { + _, err := repo.GetBySeason(NewCanceledContext(), "Test Show", 1) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("MarkWatched with cancelled context", func(t *testing.T) { + err := repo.MarkWatched(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + + t.Run("StartWatching with cancelled context", func(t *testing.T) { + err := repo.StartWatching(NewCanceledContext(), id) + AssertError(t, err, "Expected error with cancelled context") + }) + }) + + t.Run("Edge Cases", func(t *testing.T) { + db := CreateTestDB(t) + repo := NewTVRepository(db) + ctx := context.Background() + + t.Run("Get non-existent TV show", func(t *testing.T) { + _, err := repo.Get(ctx, 99999) + AssertError(t, err, "Expected error for non-existent TV show") + }) + + t.Run("Update non-existent TV show succeeds with no rows affected", func(t *testing.T) { + show := NewTVShowBuilder().WithTitle("Non-existent").Build() + show.ID = 99999 + err := repo.Update(ctx, show) + AssertNoError(t, err, "Update should not error when no rows affected") + }) + + t.Run("Delete non-existent TV show succeeds with no rows affected", func(t *testing.T) { + err := repo.Delete(ctx, 99999) + AssertNoError(t, err, "Delete should not error when no rows affected") + }) + + t.Run("MarkWatched non-existent TV show", func(t *testing.T) { + err := repo.MarkWatched(ctx, 99999) + AssertError(t, err, "Expected error for non-existent TV show") + }) + + t.Run("StartWatching non-existent TV show", func(t *testing.T) { + err := repo.StartWatching(ctx, 99999) + AssertError(t, err, "Expected error for non-existent TV show") + }) + + t.Run("GetByTitle with no results", func(t *testing.T) { + shows, err := repo.GetByTitle(ctx, "NonExistentShow") + AssertNoError(t, err, "Should not error when no shows found") + AssertEqual(t, 0, len(shows), "Expected empty result set") + }) + + t.Run("GetBySeason with no results", func(t *testing.T) { + shows, err := repo.GetBySeason(ctx, "NonExistentShow", 1) + AssertNoError(t, err, "Should not error when no shows found") + AssertEqual(t, 0, len(shows), "Expected empty result set") }) }) }