cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

refactor: test helpers & shared utilities

+1449 -1516
+18 -21
internal/articles/parser.go
··· 34 35 // ParsingRule represents XPath rules for extracting content from a specific domain 36 type ParsingRule struct { 37 - Domain string 38 - Title string 39 - Author string 40 - Date string 41 - Body string 42 - // XPath selectors for elements to remove 43 - Strip []string 44 TestURLs []string 45 } 46 ··· 152 } 153 154 // ParseURL extracts article content from a given URL 155 - func (p *ArticleParser) ParseURL(urlStr string) (*ParsedContent, error) { 156 - parsedURL, err := url.Parse(urlStr) 157 if err != nil { 158 return nil, fmt.Errorf("invalid URL: %w", err) 159 } 160 161 domain := parsedURL.Hostname() 162 163 - resp, err := p.client.Get(urlStr) 164 if err != nil { 165 return nil, fmt.Errorf("failed to fetch URL: %w", err) 166 } ··· 175 return nil, fmt.Errorf("failed to read response body: %w", err) 176 } 177 178 - return p.Parse(string(htmlBytes), domain, urlStr) 179 } 180 181 // ParseHTML extracts article content from HTML string using domain-specific rules ··· 257 } 258 259 // SaveArticle saves the parsed content to filesystem and returns file paths 260 - func (p *ArticleParser) SaveArticle(content *ParsedContent, storageDir string) (markdownPath, htmlPath string, err error) { 261 - if err := os.MkdirAll(storageDir, 0755); err != nil { 262 return "", "", fmt.Errorf("failed to create storage directory: %w", err) 263 } 264 ··· 267 slug = "article" 268 } 269 270 - baseMarkdownPath := filepath.Join(storageDir, slug+".md") 271 - baseHTMLPath := filepath.Join(storageDir, slug+".html") 272 273 markdownPath = baseMarkdownPath 274 htmlPath = baseHTMLPath ··· 280 break 281 } 282 } 283 - markdownPath = filepath.Join(storageDir, fmt.Sprintf("%s_%d.md", slug, counter)) 284 - htmlPath = filepath.Join(storageDir, fmt.Sprintf("%s_%d.html", slug, counter)) 285 counter++ 286 } 287 ··· 385 return nil, fmt.Errorf("failed to save article: %w", err) 386 } 387 388 - article := &models.Article{ 389 URL: url, 390 Title: content.Title, 391 Author: content.Author, ··· 394 HTMLPath: htmlPath, 395 Created: time.Now(), 396 Modified: time.Now(), 397 - } 398 - 399 - return article, nil 400 }
··· 34 35 // ParsingRule represents XPath rules for extracting content from a specific domain 36 type ParsingRule struct { 37 + Domain string 38 + Title string 39 + Author string 40 + Date string 41 + Body string 42 + Strip []string // XPath selectors for elements to remove 43 TestURLs []string 44 } 45 ··· 151 } 152 153 // ParseURL extracts article content from a given URL 154 + func (p *ArticleParser) ParseURL(s string) (*ParsedContent, error) { 155 + parsedURL, err := url.Parse(s) 156 if err != nil { 157 return nil, fmt.Errorf("invalid URL: %w", err) 158 } 159 160 domain := parsedURL.Hostname() 161 162 + resp, err := p.client.Get(s) 163 if err != nil { 164 return nil, fmt.Errorf("failed to fetch URL: %w", err) 165 } ··· 174 return nil, fmt.Errorf("failed to read response body: %w", err) 175 } 176 177 + return p.Parse(string(htmlBytes), domain, s) 178 } 179 180 // ParseHTML extracts article content from HTML string using domain-specific rules ··· 256 } 257 258 // SaveArticle saves the parsed content to filesystem and returns file paths 259 + func (p *ArticleParser) SaveArticle(content *ParsedContent, dir string) (markdownPath, htmlPath string, err error) { 260 + if err := os.MkdirAll(dir, 0755); err != nil { 261 return "", "", fmt.Errorf("failed to create storage directory: %w", err) 262 } 263 ··· 266 slug = "article" 267 } 268 269 + baseMarkdownPath := filepath.Join(dir, slug+".md") 270 + baseHTMLPath := filepath.Join(dir, slug+".html") 271 272 markdownPath = baseMarkdownPath 273 htmlPath = baseHTMLPath ··· 279 break 280 } 281 } 282 + markdownPath = filepath.Join(dir, fmt.Sprintf("%s_%d.md", slug, counter)) 283 + htmlPath = filepath.Join(dir, fmt.Sprintf("%s_%d.html", slug, counter)) 284 counter++ 285 } 286 ··· 384 return nil, fmt.Errorf("failed to save article: %w", err) 385 } 386 387 + return &models.Article{ 388 URL: url, 389 Title: content.Title, 390 Author: content.Author, ··· 393 HTMLPath: htmlPath, 394 Created: time.Now(), 395 Modified: time.Now(), 396 + }, nil 397 }
+7 -15
internal/articles/parser_test.go
··· 161 t.Run("slugify", func(t *testing.T) { 162 parser := &ArticleParser{} 163 164 - testCases := []struct { 165 input string 166 expected string 167 }{ ··· 174 {strings.Repeat("a", 150), strings.Repeat("a", 100)}, 175 } 176 177 - for _, tc := range testCases { 178 - t.Run(fmt.Sprintf("slugify '%s'", tc.input), func(t *testing.T) { 179 - result := parser.slugify(tc.input) 180 - if result != tc.expected { 181 - t.Errorf("Expected '%s', got '%s'", tc.expected, result) 182 } 183 }) 184 } ··· 536 })) 537 defer server.Close() 538 539 - // Use a direct Wikipedia URL that would be processed by the real function 540 _, err := CreateArticleFromURL("https://en.wikipedia.org/wiki/NonExistentPage12345", tempDir) 541 if err == nil { 542 t.Error("Expected error for HTTP 404") ··· 547 }) 548 549 t.Run("fails with network error", func(t *testing.T) { 550 - // Use a non-existent server to trigger network error 551 _, err := CreateArticleFromURL("http://localhost:99999/test", tempDir) 552 if err == nil { 553 t.Error("Expected error for network failure") ··· 565 t.Run("fails with malformed HTML", func(t *testing.T) { 566 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 567 w.WriteHeader(http.StatusOK) 568 - w.Write([]byte("<html><head><title>Test</head></body>")) // Malformed HTML 569 })) 570 defer server.Close() 571 572 - // Create a custom parser with localhost rule for testing 573 parser, err := NewArticleParser(server.Client()) 574 if err != nil { 575 t.Fatalf("Failed to create parser: %v", err) ··· 587 if err == nil { 588 t.Error("Expected error for malformed HTML") 589 } 590 - // Malformed HTML may either fail to parse or fail to extract title 591 if !strings.Contains(err.Error(), "failed to parse HTML") && !strings.Contains(err.Error(), "could not extract title") { 592 t.Errorf("Expected HTML parsing or title extraction error, got %v", err) 593 } ··· 607 })) 608 defer server.Close() 609 610 - // Create a custom parser with localhost rule for testing 611 parser, err := NewArticleParser(server.Client()) 612 if err != nil { 613 t.Fatalf("Failed to create parser: %v", err) ··· 646 server := newServerWithHtml(wikipediaHTML) 647 defer server.Close() 648 649 - // Create a custom parser with localhost rule for testing 650 parser, err := NewArticleParser(server.Client()) 651 if err != nil { 652 t.Fatalf("Failed to create parser: %v", err) ··· 699 t.Error("Expected Modified timestamp to be set") 700 } 701 702 - // Check files exist 703 if _, err := os.Stat(article.MarkdownPath); os.IsNotExist(err) { 704 t.Error("Expected markdown file to exist") 705 } ··· 707 t.Error("Expected HTML file to exist") 708 } 709 710 - // Verify file contents 711 mdContent, err := os.ReadFile(article.MarkdownPath) 712 if err != nil { 713 t.Fatalf("Failed to read markdown file: %v", err)
··· 161 t.Run("slugify", func(t *testing.T) { 162 parser := &ArticleParser{} 163 164 + tc := []struct { 165 input string 166 expected string 167 }{ ··· 174 {strings.Repeat("a", 150), strings.Repeat("a", 100)}, 175 } 176 177 + for _, tt := range tc { 178 + t.Run(fmt.Sprintf("slugify '%s'", tt.input), func(t *testing.T) { 179 + result := parser.slugify(tt.input) 180 + if result != tt.expected { 181 + t.Errorf("Expected '%s', got '%s'", tt.expected, result) 182 } 183 }) 184 } ··· 536 })) 537 defer server.Close() 538 539 _, err := CreateArticleFromURL("https://en.wikipedia.org/wiki/NonExistentPage12345", tempDir) 540 if err == nil { 541 t.Error("Expected error for HTTP 404") ··· 546 }) 547 548 t.Run("fails with network error", func(t *testing.T) { 549 _, err := CreateArticleFromURL("http://localhost:99999/test", tempDir) 550 if err == nil { 551 t.Error("Expected error for network failure") ··· 563 t.Run("fails with malformed HTML", func(t *testing.T) { 564 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 565 w.WriteHeader(http.StatusOK) 566 + w.Write([]byte("<html><head><title>Test</head></body>")) 567 })) 568 defer server.Close() 569 570 parser, err := NewArticleParser(server.Client()) 571 if err != nil { 572 t.Fatalf("Failed to create parser: %v", err) ··· 584 if err == nil { 585 t.Error("Expected error for malformed HTML") 586 } 587 if !strings.Contains(err.Error(), "failed to parse HTML") && !strings.Contains(err.Error(), "could not extract title") { 588 t.Errorf("Expected HTML parsing or title extraction error, got %v", err) 589 } ··· 603 })) 604 defer server.Close() 605 606 parser, err := NewArticleParser(server.Client()) 607 if err != nil { 608 t.Fatalf("Failed to create parser: %v", err) ··· 641 server := newServerWithHtml(wikipediaHTML) 642 defer server.Close() 643 644 parser, err := NewArticleParser(server.Client()) 645 if err != nil { 646 t.Fatalf("Failed to create parser: %v", err) ··· 693 t.Error("Expected Modified timestamp to be set") 694 } 695 696 if _, err := os.Stat(article.MarkdownPath); os.IsNotExist(err) { 697 t.Error("Expected markdown file to exist") 698 } ··· 700 t.Error("Expected HTML file to exist") 701 } 702 703 mdContent, err := os.ReadFile(article.MarkdownPath) 704 if err != nil { 705 t.Fatalf("Failed to read markdown file: %v", err)
+37 -36
internal/handlers/articles_test.go
··· 12 "github.com/stormlightlabs/noteleaf/internal/articles" 13 "github.com/stormlightlabs/noteleaf/internal/models" 14 "github.com/stormlightlabs/noteleaf/internal/repo" 15 ) 16 17 func TestArticleHandler(t *testing.T) { ··· 49 } 50 51 _, err := NewArticleHandler() 52 - Expect.AssertError(t, err, "failed to initialize database", "NewArticleHandler should fail when database initialization fails") 53 }) 54 55 }) ··· 85 helper.AddTestRule("127.0.0.1", testRule) 86 87 err := helper.Add(ctx, server.URL+"/test-article") 88 - Expect.AssertNoError(t, err, "Add should succeed with valid URL") 89 90 articles, err := helper.repos.Articles.List(ctx, &repo.ArticleListOptions{}) 91 if err != nil { ··· 128 } 129 130 err = helper.Add(ctx, duplicateURL) 131 - Expect.AssertNoError(t, err, "Add should succeed with duplicate URL and return existing") 132 }) 133 134 t.Run("handles unsupported domain", func(t *testing.T) { ··· 142 defer server.Close() 143 144 err := helper.Add(ctx, server.URL+"/unsupported") 145 - Expect.AssertError(t, err, "failed to parse article", "Add should fail with unsupported domain") 146 }) 147 148 t.Run("handles HTTP error", func(t *testing.T) { ··· 155 defer server.Close() 156 157 err := helper.Add(ctx, server.URL+"/404") 158 - Expect.AssertError(t, err, "failed to parse article", "Add should fail with HTTP error") 159 }) 160 161 t.Run("handles storage directory error", func(t *testing.T) { ··· 180 } 181 182 err := helper.Add(ctx, "https://example.com/test-article") 183 - Expect.AssertError(t, err, "failed to get article storage dir", "Add should fail when storage directory cannot be determined") 184 }) 185 186 t.Run("handles database save error", func(t *testing.T) { ··· 209 helper.db.Exec("DROP TABLE articles") 210 211 err := helper.Add(ctx, server.URL+"/test") 212 - Expect.AssertError(t, err, "failed to save article to database", "Add should fail when database save fails") 213 }) 214 }) 215 ··· 222 id2 := helper.CreateTestArticle(t, "https://example.com/article2", "Second Article", "Jane Smith", "2024-01-02") 223 224 err := helper.List(ctx, "", "", 0) 225 - Expect.AssertNoError(t, err, "List should succeed") 226 227 AssertExists(t, helper.repos.Articles.Get, id1, "article") 228 AssertExists(t, helper.repos.Articles.Get, id2, "article") ··· 236 helper.CreateTestArticle(t, "https://example.com/second", "Second Article", "Jane", "2024-01-02") 237 238 err := helper.List(ctx, "First", "", 0) 239 - Expect.AssertNoError(t, err, "List with title filter should succeed") 240 }) 241 242 t.Run("lists with author filter", func(t *testing.T) { ··· 247 helper.CreateTestArticle(t, "https://example.com/jane1", "Article by Jane", "Jane Smith", "2024-01-02") 248 249 err := helper.List(ctx, "", "John", 0) 250 - Expect.AssertNoError(t, err, "List with author filter should succeed") 251 }) 252 253 t.Run("lists with limit", func(t *testing.T) { ··· 259 helper.CreateTestArticle(t, "https://example.com/3", "Article 3", "Author", "2024-01-03") 260 261 err := helper.List(ctx, "", "", 2) 262 - Expect.AssertNoError(t, err, "List with limit should succeed") 263 }) 264 265 t.Run("handles empty results", func(t *testing.T) { ··· 267 ctx := context.Background() 268 269 err := helper.List(ctx, "nonexistent", "", 0) 270 - Expect.AssertNoError(t, err, "List with no matches should succeed") 271 }) 272 273 t.Run("handles database error", func(t *testing.T) { ··· 277 helper.db.Exec("DROP TABLE articles") 278 279 err := helper.List(ctx, "", "", 0) 280 - Expect.AssertError(t, err, "failed to list articles", "List should fail when database is corrupted") 281 }) 282 }) 283 ··· 289 id := helper.CreateTestArticle(t, "https://example.com/test", "Test Article", "Test Author", "2024-01-01") 290 291 err := helper.View(ctx, id) 292 - Expect.AssertNoError(t, err, "View should succeed with valid article ID") 293 }) 294 295 t.Run("handles non-existent article", func(t *testing.T) { ··· 297 ctx := context.Background() 298 299 err := helper.View(ctx, 99999) 300 - Expect.AssertError(t, err, "failed to get article", "View should fail with non-existent article ID") 301 }) 302 303 t.Run("handles missing files gracefully", func(t *testing.T) { ··· 321 } 322 323 err = helper.View(ctx, id) 324 - Expect.AssertNoError(t, err, "View should succeed even when files are missing") 325 }) 326 327 t.Run("handles database error", func(t *testing.T) { ··· 331 helper.db.Exec("DROP TABLE articles") 332 333 err := helper.View(ctx, 1) 334 - Expect.AssertError(t, err, "failed to get article", "View should fail when database is corrupted") 335 }) 336 }) 337 ··· 341 ctx := context.Background() 342 id := helper.CreateTestArticle(t, "https://example.com/read", "Read Test Article", "Test Author", "2024-01-01") 343 err := helper.Read(ctx, id) 344 - Expect.AssertNoError(t, err, "Read should succeed with valid article ID") 345 }) 346 347 t.Run("handles non-existent article", func(t *testing.T) { 348 helper := NewArticleTestHelper(t) 349 ctx := context.Background() 350 err := helper.Read(ctx, 99999) 351 - Expect.AssertError(t, err, "failed to get article", "Read should fail with non-existent article ID") 352 }) 353 354 t.Run("handles missing markdown file", func(t *testing.T) { ··· 372 } 373 374 err = helper.Read(ctx, id) 375 - Expect.AssertError(t, err, "markdown file not found", "Read should fail when markdown file is missing") 376 }) 377 378 t.Run("handles database error", func(t *testing.T) { ··· 380 ctx := context.Background() 381 helper.db.Exec("DROP TABLE articles") 382 err := helper.Read(ctx, 1) 383 - Expect.AssertError(t, err, "failed to get article", "Read should fail when database is corrupted") 384 }) 385 }) 386 ··· 392 AssertExists(t, helper.repos.Articles.Get, id, "article") 393 394 err := helper.Remove(ctx, id) 395 - Expect.AssertNoError(t, err, "Remove should succeed") 396 AssertNotExists(t, helper.repos.Articles.Get, id, "article") 397 }) 398 ··· 400 helper := NewArticleTestHelper(t) 401 ctx := context.Background() 402 err := helper.Remove(ctx, 99999) 403 - Expect.AssertError(t, err, "failed to get article", "Remove should fail with non-existent article ID") 404 }) 405 406 t.Run("handles missing files gracefully", func(t *testing.T) { ··· 424 } 425 426 err = helper.Remove(ctx, id) 427 - Expect.AssertNoError(t, err, "Remove should succeed even when files don't exist") 428 }) 429 430 t.Run("handles database error", func(t *testing.T) { ··· 435 helper.db.Exec("DROP TABLE articles") 436 437 err := helper.Remove(ctx, id) 438 - Expect.AssertError(t, err, "failed to get article", "Remove should fail when database is corrupted") 439 }) 440 }) 441 ··· 443 t.Run("shows supported domains", func(t *testing.T) { 444 helper := NewArticleTestHelper(t) 445 err := helper.Help() 446 - Expect.AssertNoError(t, err, "Help should succeed") 447 }) 448 449 t.Run("handles storage directory error", func(t *testing.T) { ··· 467 } 468 469 err := helper.Help() 470 - Expect.AssertError(t, err, "failed to get storage directory", "Help should fail when storage directory cannot be determined") 471 }) 472 }) 473 ··· 475 t.Run("closes successfully", func(t *testing.T) { 476 helper := NewArticleTestHelper(t) 477 err := helper.Close() 478 - Expect.AssertNoError(t, err, "Close should succeed") 479 }) 480 481 t.Run("handles nil database gracefully", func(t *testing.T) { 482 helper := NewArticleTestHelper(t) 483 helper.db = nil 484 err := helper.Close() 485 - Expect.AssertNoError(t, err, "Close should succeed with nil database") 486 }) 487 }) 488 ··· 490 t.Run("returns storage directory successfully", func(t *testing.T) { 491 helper := NewArticleTestHelper(t) 492 dir, err := helper.getStorageDirectory() 493 - Expect.AssertNoError(t, err, "getStorageDirectory should succeed") 494 495 if dir == "" { 496 t.Error("Storage directory should not be empty") ··· 522 } 523 524 _, err := helper.getStorageDirectory() 525 - Expect.AssertError(t, err, "", "getStorageDirectory should fail when home directory cannot be determined") 526 }) 527 }) 528 } ··· 556 helper.AddTestRule("127.0.0.1", testRule) 557 558 err := helper.Add(ctx, server.URL+"/integration-test") 559 - Expect.AssertNoError(t, err, "Add should succeed in integration test") 560 561 err = helper.List(ctx, "", "", 0) 562 - Expect.AssertNoError(t, err, "List should succeed in integration test") 563 564 articles, err := helper.repos.Articles.List(ctx, &repo.ArticleListOptions{}) 565 if err != nil { ··· 573 articleID := articles[0].ID 574 575 err = helper.View(ctx, articleID) 576 - Expect.AssertNoError(t, err, "View should succeed in integration test") 577 578 err = helper.Help() 579 - Expect.AssertNoError(t, err, "Help should succeed in integration test") 580 581 err = helper.Remove(ctx, articleID) 582 - Expect.AssertNoError(t, err, "Remove should succeed in integration test") 583 584 AssertNotExists(t, helper.repos.Articles.Get, articleID, "article") 585 })
··· 12 "github.com/stormlightlabs/noteleaf/internal/articles" 13 "github.com/stormlightlabs/noteleaf/internal/models" 14 "github.com/stormlightlabs/noteleaf/internal/repo" 15 + "github.com/stormlightlabs/noteleaf/internal/shared" 16 ) 17 18 func TestArticleHandler(t *testing.T) { ··· 50 } 51 52 _, err := NewArticleHandler() 53 + shared.AssertErrorContains(t, err, "failed to initialize database", "NewArticleHandler should fail when database initialization fails") 54 }) 55 56 }) ··· 86 helper.AddTestRule("127.0.0.1", testRule) 87 88 err := helper.Add(ctx, server.URL+"/test-article") 89 + shared.AssertNoError(t, err, "Add should succeed with valid URL") 90 91 articles, err := helper.repos.Articles.List(ctx, &repo.ArticleListOptions{}) 92 if err != nil { ··· 129 } 130 131 err = helper.Add(ctx, duplicateURL) 132 + shared.AssertNoError(t, err, "Add should succeed with duplicate URL and return existing") 133 }) 134 135 t.Run("handles unsupported domain", func(t *testing.T) { ··· 143 defer server.Close() 144 145 err := helper.Add(ctx, server.URL+"/unsupported") 146 + shared.AssertErrorContains(t, err, "failed to parse article", "Add should fail with unsupported domain") 147 }) 148 149 t.Run("handles HTTP error", func(t *testing.T) { ··· 156 defer server.Close() 157 158 err := helper.Add(ctx, server.URL+"/404") 159 + shared.AssertErrorContains(t, err, "failed to parse article", "Add should fail with HTTP error") 160 }) 161 162 t.Run("handles storage directory error", func(t *testing.T) { ··· 181 } 182 183 err := helper.Add(ctx, "https://example.com/test-article") 184 + shared.AssertErrorContains(t, err, "failed to get article storage dir", "Add should fail when storage directory cannot be determined") 185 }) 186 187 t.Run("handles database save error", func(t *testing.T) { ··· 210 helper.db.Exec("DROP TABLE articles") 211 212 err := helper.Add(ctx, server.URL+"/test") 213 + shared.AssertErrorContains(t, err, "failed to save article to database", "Add should fail when database save fails") 214 }) 215 }) 216 ··· 223 id2 := helper.CreateTestArticle(t, "https://example.com/article2", "Second Article", "Jane Smith", "2024-01-02") 224 225 err := helper.List(ctx, "", "", 0) 226 + shared.AssertNoError(t, err, "List should succeed") 227 228 AssertExists(t, helper.repos.Articles.Get, id1, "article") 229 AssertExists(t, helper.repos.Articles.Get, id2, "article") ··· 237 helper.CreateTestArticle(t, "https://example.com/second", "Second Article", "Jane", "2024-01-02") 238 239 err := helper.List(ctx, "First", "", 0) 240 + shared.AssertNoError(t, err, "List with title filter should succeed") 241 }) 242 243 t.Run("lists with author filter", func(t *testing.T) { ··· 248 helper.CreateTestArticle(t, "https://example.com/jane1", "Article by Jane", "Jane Smith", "2024-01-02") 249 250 err := helper.List(ctx, "", "John", 0) 251 + shared.AssertNoError(t, err, "List with author filter should succeed") 252 }) 253 254 t.Run("lists with limit", func(t *testing.T) { ··· 260 helper.CreateTestArticle(t, "https://example.com/3", "Article 3", "Author", "2024-01-03") 261 262 err := helper.List(ctx, "", "", 2) 263 + shared.AssertNoError(t, err, "List with limit should succeed") 264 }) 265 266 t.Run("handles empty results", func(t *testing.T) { ··· 268 ctx := context.Background() 269 270 err := helper.List(ctx, "nonexistent", "", 0) 271 + shared.AssertNoError(t, err, "List with no matches should succeed") 272 }) 273 274 t.Run("handles database error", func(t *testing.T) { ··· 278 helper.db.Exec("DROP TABLE articles") 279 280 err := helper.List(ctx, "", "", 0) 281 + shared.AssertErrorContains(t, err, "failed to list articles", "List should fail when database is corrupted") 282 }) 283 }) 284 ··· 290 id := helper.CreateTestArticle(t, "https://example.com/test", "Test Article", "Test Author", "2024-01-01") 291 292 err := helper.View(ctx, id) 293 + shared.AssertNoError(t, err, "View should succeed with valid article ID") 294 }) 295 296 t.Run("handles non-existent article", func(t *testing.T) { ··· 298 ctx := context.Background() 299 300 err := helper.View(ctx, 99999) 301 + shared.AssertErrorContains(t, err, "failed to get article", "View should fail with non-existent article ID") 302 }) 303 304 t.Run("handles missing files gracefully", func(t *testing.T) { ··· 322 } 323 324 err = helper.View(ctx, id) 325 + shared.AssertNoError(t, err, "View should succeed even when files are missing") 326 }) 327 328 t.Run("handles database error", func(t *testing.T) { ··· 332 helper.db.Exec("DROP TABLE articles") 333 334 err := helper.View(ctx, 1) 335 + shared.AssertErrorContains(t, err, "failed to get article", "View should fail when database is corrupted") 336 }) 337 }) 338 ··· 342 ctx := context.Background() 343 id := helper.CreateTestArticle(t, "https://example.com/read", "Read Test Article", "Test Author", "2024-01-01") 344 err := helper.Read(ctx, id) 345 + shared.AssertNoError(t, err, "Read should succeed with valid article ID") 346 }) 347 348 t.Run("handles non-existent article", func(t *testing.T) { 349 helper := NewArticleTestHelper(t) 350 ctx := context.Background() 351 err := helper.Read(ctx, 99999) 352 + shared.AssertErrorContains(t, err, "failed to get article", "Read should fail with non-existent article ID") 353 }) 354 355 t.Run("handles missing markdown file", func(t *testing.T) { ··· 373 } 374 375 err = helper.Read(ctx, id) 376 + shared.AssertErrorContains(t, err, "markdown file not found", "Read should fail when markdown file is missing") 377 }) 378 379 t.Run("handles database error", func(t *testing.T) { ··· 381 ctx := context.Background() 382 helper.db.Exec("DROP TABLE articles") 383 err := helper.Read(ctx, 1) 384 + shared.AssertErrorContains(t, err, "failed to get article", "Read should fail when database is corrupted") 385 }) 386 }) 387 ··· 393 AssertExists(t, helper.repos.Articles.Get, id, "article") 394 395 err := helper.Remove(ctx, id) 396 + shared.AssertNoError(t, err, "Remove should succeed") 397 AssertNotExists(t, helper.repos.Articles.Get, id, "article") 398 }) 399 ··· 401 helper := NewArticleTestHelper(t) 402 ctx := context.Background() 403 err := helper.Remove(ctx, 99999) 404 + shared.AssertErrorContains(t, err, "failed to get article", "Remove should fail with non-existent article ID") 405 }) 406 407 t.Run("handles missing files gracefully", func(t *testing.T) { ··· 425 } 426 427 err = helper.Remove(ctx, id) 428 + shared.AssertNoError(t, err, "Remove should succeed even when files don't exist") 429 }) 430 431 t.Run("handles database error", func(t *testing.T) { ··· 436 helper.db.Exec("DROP TABLE articles") 437 438 err := helper.Remove(ctx, id) 439 + shared.AssertErrorContains(t, err, "failed to get article", "Remove should fail when database is corrupted") 440 }) 441 }) 442 ··· 444 t.Run("shows supported domains", func(t *testing.T) { 445 helper := NewArticleTestHelper(t) 446 err := helper.Help() 447 + shared.AssertNoError(t, err, "Help should succeed") 448 }) 449 450 t.Run("handles storage directory error", func(t *testing.T) { ··· 468 } 469 470 err := helper.Help() 471 + shared.AssertErrorContains(t, err, "failed to get storage directory", "Help should fail when storage directory cannot be determined") 472 }) 473 }) 474 ··· 476 t.Run("closes successfully", func(t *testing.T) { 477 helper := NewArticleTestHelper(t) 478 err := helper.Close() 479 + shared.AssertNoError(t, err, "Close should succeed") 480 }) 481 482 t.Run("handles nil database gracefully", func(t *testing.T) { 483 helper := NewArticleTestHelper(t) 484 helper.db = nil 485 err := helper.Close() 486 + shared.AssertNoError(t, err, "Close should succeed with nil database") 487 }) 488 }) 489 ··· 491 t.Run("returns storage directory successfully", func(t *testing.T) { 492 helper := NewArticleTestHelper(t) 493 dir, err := helper.getStorageDirectory() 494 + shared.AssertNoError(t, err, "getStorageDirectory should succeed") 495 496 if dir == "" { 497 t.Error("Storage directory should not be empty") ··· 523 } 524 525 _, err := helper.getStorageDirectory() 526 + shared.AssertErrorContains(t, err, "", "getStorageDirectory should fail when home directory cannot be determined") 527 }) 528 }) 529 } ··· 557 helper.AddTestRule("127.0.0.1", testRule) 558 559 err := helper.Add(ctx, server.URL+"/integration-test") 560 + shared.AssertNoError(t, err, "Add should succeed in integration test") 561 562 err = helper.List(ctx, "", "", 0) 563 + shared.AssertNoError(t, err, "List should succeed in integration test") 564 565 articles, err := helper.repos.Articles.List(ctx, &repo.ArticleListOptions{}) 566 if err != nil { ··· 574 articleID := articles[0].ID 575 576 err = helper.View(ctx, articleID) 577 + shared.AssertNoError(t, err, "View should succeed in integration test") 578 579 err = helper.Help() 580 + shared.AssertNoError(t, err, "Help should succeed in integration test") 581 582 err = helper.Remove(ctx, articleID) 583 + shared.AssertNoError(t, err, "Remove should succeed in integration test") 584 585 AssertNotExists(t, helper.repos.Articles.Get, articleID, "article") 586 })
+10 -31
internal/handlers/books_test.go
··· 11 "github.com/stormlightlabs/noteleaf/internal/models" 12 "github.com/stormlightlabs/noteleaf/internal/repo" 13 "github.com/stormlightlabs/noteleaf/internal/services" 14 ) 15 - 16 - // setupBookTest removed - use NewHandlerTestSuite(t) instead 17 18 func createTestBook(t *testing.T, handler *BookHandler, ctx context.Context) *models.Book { 19 t.Helper() ··· 113 }) 114 115 t.Run("handles HTTP error responses", func(t *testing.T) { 116 - mockServer := HTTPErrorMockServer(500, "Internal Server Error") 117 defer mockServer.Close() 118 119 handler.service = services.NewBookService(mockServer.URL()) ··· 129 }) 130 131 t.Run("handles malformed JSON response", func(t *testing.T) { 132 - mockServer := InvalidJSONMockServer() 133 defer mockServer.Close() 134 135 handler.service = services.NewBookService(mockServer.URL()) ··· 149 NumFound: 0, Start: 0, Docs: []services.OpenLibrarySearchDoc{}, 150 } 151 152 - mockServer := JSONMockServer(emptyResponse) 153 defer mockServer.Close() 154 155 handler.service = services.NewBookService(mockServer.URL()) ··· 163 }) 164 165 t.Run("handles network timeouts", func(t *testing.T) { 166 - mockServer := TimeoutMockServer(5 * time.Second) 167 defer mockServer.Close() 168 169 handler.service = services.NewBookService(mockServer.URL()) ··· 181 {Key: "/works/OL123456W", Title: "Test Book", Authors: []string{"Author"}, Year: 2020}, 182 } 183 mockResponse := MockOpenLibraryResponse(mockBooks) 184 - mockServer := JSONMockServer(mockResponse) 185 defer mockServer.Close() 186 187 handler.service = services.NewBookService(mockServer.URL()) ··· 249 {Key: "/works/OL456W", Title: "Test Book 2", Authors: []string{"Author 2"}, Year: 2021, Editions: 3, CoverID: 456}, 250 } 251 mockResponse := MockOpenLibraryResponse(mockBooks) 252 - mockServer := JSONMockServer(mockResponse) 253 defer mockServer.Close() 254 255 handler.service = services.NewBookService(mockServer.URL()) ··· 279 {Key: "/works/OL789W", Title: "Another Book", Authors: []string{"Another Author"}, Year: 2022}, 280 } 281 mockResponse := MockOpenLibraryResponse(mockBooks) 282 - mockServer := JSONMockServer(mockResponse) 283 defer mockServer.Close() 284 285 handler.service = services.NewBookService(mockServer.URL()) ··· 307 {Key: "/works/OL999W", Title: "Choice Test Book", Authors: []string{"Choice Author"}, Year: 2023}, 308 } 309 mockResponse := MockOpenLibraryResponse(mockBooks) 310 - mockServer := JSONMockServer(mockResponse) 311 defer mockServer.Close() 312 313 handler.service = services.NewBookService(mockServer.URL()) ··· 821 }) 822 } 823 824 - // TestBookHandlerWithSuite demonstrates using HandlerTestSuite for cleaner tests 825 func TestBookHandlerWithSuite(t *testing.T) { 826 - // Example: Using HandlerTestSuite for lifecycle testing 827 t.Run("Lifecycle", func(t *testing.T) { 828 suite := NewMediaHandlerTestSuite(t) 829 ··· 831 suite.AssertNoError(err, "NewBookHandler") 832 defer handler.Close() 833 834 - // Test basic behaviors using reusable patterns 835 InputReaderTest(t, handler) 836 }) 837 838 - // Example: Using generic CreateHandler 839 t.Run("GenericHandlerCreation", func(t *testing.T) { 840 _ = NewHandlerTestSuite(t) 841 - 842 - // Generic CreateHandler replaces CreateBookHandler 843 handler := CreateHandler(t, NewBookHandler) 844 - 845 - // Handler automatically cleaned up via t.Cleanup 846 _ = handler 847 }) 848 849 - // Example: Using HandlerTestSuite for media operations 850 t.Run("MediaOperations", func(t *testing.T) { 851 suite := NewMediaHandlerTestSuite(t) 852 ··· 854 suite.AssertNoError(err, "NewBookHandler") 855 defer handler.Close() 856 857 - // Create a test book first 858 book := &models.Book{ 859 Title: "Suite Test Book", 860 Author: "Suite Test Author", ··· 862 Added: time.Now(), 863 } 864 id, err := handler.repos.Books.Create(suite.Context(), book) 865 - suite.AssertNoError(err, "Create test book") 866 867 - // Test list operation 868 suite.TestList(handler, "") 869 - 870 - // Test status update 871 suite.TestUpdateStatus(handler, strconv.FormatInt(id, 10), "reading", true) 872 - 873 - // Test invalid status update 874 suite.TestUpdateStatus(handler, strconv.FormatInt(id, 10), "invalid", false) 875 - 876 - // Test remove operation 877 suite.TestRemove(handler, strconv.FormatInt(id, 10), true) 878 }) 879 880 - // Example: Generic lifecycle test 881 t.Run("GenericLifecycle", func(t *testing.T) { 882 _ = NewHandlerTestSuite(t) 883 - 884 - // Demonstrates using generic HandlerLifecycleTest 885 HandlerLifecycleTest(t, NewBookHandler) 886 }) 887 }
··· 11 "github.com/stormlightlabs/noteleaf/internal/models" 12 "github.com/stormlightlabs/noteleaf/internal/repo" 13 "github.com/stormlightlabs/noteleaf/internal/services" 14 + "github.com/stormlightlabs/noteleaf/internal/shared" 15 ) 16 17 func createTestBook(t *testing.T, handler *BookHandler, ctx context.Context) *models.Book { 18 t.Helper() ··· 112 }) 113 114 t.Run("handles HTTP error responses", func(t *testing.T) { 115 + mockServer := shared.HTTPErrorMockServer(500, "Internal Server Error") 116 defer mockServer.Close() 117 118 handler.service = services.NewBookService(mockServer.URL()) ··· 128 }) 129 130 t.Run("handles malformed JSON response", func(t *testing.T) { 131 + mockServer := shared.InvalidJSONMockServer() 132 defer mockServer.Close() 133 134 handler.service = services.NewBookService(mockServer.URL()) ··· 148 NumFound: 0, Start: 0, Docs: []services.OpenLibrarySearchDoc{}, 149 } 150 151 + mockServer := shared.JSONMockServer(emptyResponse) 152 defer mockServer.Close() 153 154 handler.service = services.NewBookService(mockServer.URL()) ··· 162 }) 163 164 t.Run("handles network timeouts", func(t *testing.T) { 165 + mockServer := shared.TimeoutMockServer(5 * time.Second) 166 defer mockServer.Close() 167 168 handler.service = services.NewBookService(mockServer.URL()) ··· 180 {Key: "/works/OL123456W", Title: "Test Book", Authors: []string{"Author"}, Year: 2020}, 181 } 182 mockResponse := MockOpenLibraryResponse(mockBooks) 183 + mockServer := shared.JSONMockServer(mockResponse) 184 defer mockServer.Close() 185 186 handler.service = services.NewBookService(mockServer.URL()) ··· 248 {Key: "/works/OL456W", Title: "Test Book 2", Authors: []string{"Author 2"}, Year: 2021, Editions: 3, CoverID: 456}, 249 } 250 mockResponse := MockOpenLibraryResponse(mockBooks) 251 + mockServer := shared.JSONMockServer(mockResponse) 252 defer mockServer.Close() 253 254 handler.service = services.NewBookService(mockServer.URL()) ··· 278 {Key: "/works/OL789W", Title: "Another Book", Authors: []string{"Another Author"}, Year: 2022}, 279 } 280 mockResponse := MockOpenLibraryResponse(mockBooks) 281 + mockServer := shared.JSONMockServer(mockResponse) 282 defer mockServer.Close() 283 284 handler.service = services.NewBookService(mockServer.URL()) ··· 306 {Key: "/works/OL999W", Title: "Choice Test Book", Authors: []string{"Choice Author"}, Year: 2023}, 307 } 308 mockResponse := MockOpenLibraryResponse(mockBooks) 309 + mockServer := shared.JSONMockServer(mockResponse) 310 defer mockServer.Close() 311 312 handler.service = services.NewBookService(mockServer.URL()) ··· 820 }) 821 } 822 823 func TestBookHandlerWithSuite(t *testing.T) { 824 t.Run("Lifecycle", func(t *testing.T) { 825 suite := NewMediaHandlerTestSuite(t) 826 ··· 828 suite.AssertNoError(err, "NewBookHandler") 829 defer handler.Close() 830 831 InputReaderTest(t, handler) 832 }) 833 834 t.Run("GenericHandlerCreation", func(t *testing.T) { 835 _ = NewHandlerTestSuite(t) 836 handler := CreateHandler(t, NewBookHandler) 837 _ = handler 838 }) 839 840 t.Run("MediaOperations", func(t *testing.T) { 841 suite := NewMediaHandlerTestSuite(t) 842 ··· 844 suite.AssertNoError(err, "NewBookHandler") 845 defer handler.Close() 846 847 book := &models.Book{ 848 Title: "Suite Test Book", 849 Author: "Suite Test Author", ··· 851 Added: time.Now(), 852 } 853 id, err := handler.repos.Books.Create(suite.Context(), book) 854 855 + suite.AssertNoError(err, "Create test book") 856 suite.TestList(handler, "") 857 suite.TestUpdateStatus(handler, strconv.FormatInt(id, 10), "reading", true) 858 suite.TestUpdateStatus(handler, strconv.FormatInt(id, 10), "invalid", false) 859 suite.TestRemove(handler, strconv.FormatInt(id, 10), true) 860 }) 861 862 t.Run("GenericLifecycle", func(t *testing.T) { 863 _ = NewHandlerTestSuite(t) 864 HandlerLifecycleTest(t, NewBookHandler) 865 }) 866 }
+239 -261
internal/handlers/config_test.go
··· 8 "strings" 9 "testing" 10 11 "github.com/stormlightlabs/noteleaf/internal/store" 12 ) 13 14 - func TestConfigHandlerGet(t *testing.T) { 15 - tempDir, err := os.MkdirTemp("", "noteleaf-config-handler-get-test-*") 16 - if err != nil { 17 - t.Fatalf("Failed to create temp directory: %v", err) 18 - } 19 - defer os.RemoveAll(tempDir) 20 21 - // Set up environment 22 - customConfigPath := filepath.Join(tempDir, "test-config.toml") 23 - originalEnv := os.Getenv("NOTELEAF_CONFIG") 24 - os.Setenv("NOTELEAF_CONFIG", customConfigPath) 25 - defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 26 27 - // Create a test config 28 - config := store.DefaultConfig() 29 - config.ColorScheme = "test-scheme" 30 - config.Editor = "vim" 31 - if err := store.SaveConfig(config); err != nil { 32 - t.Fatalf("Failed to save config: %v", err) 33 - } 34 - 35 - t.Run("Get all config values", func(t *testing.T) { 36 - handler, err := NewConfigHandler() 37 - if err != nil { 38 - t.Fatalf("Failed to create handler: %v", err) 39 } 40 41 - // Capture stdout 42 - oldStdout := os.Stdout 43 - r, w, _ := os.Pipe() 44 - os.Stdout = w 45 46 - err = handler.Get("") 47 - if err != nil { 48 - t.Fatalf("Get failed: %v", err) 49 - } 50 51 - w.Close() 52 - os.Stdout = oldStdout 53 54 - var buf bytes.Buffer 55 - io.Copy(&buf, r) 56 - output := buf.String() 57 58 - if !strings.Contains(output, "color_scheme") { 59 - t.Error("Output should contain color_scheme") 60 - } 61 - if !strings.Contains(output, "test-scheme") { 62 - t.Error("Output should contain test-scheme value") 63 - } 64 - }) 65 66 - t.Run("Get specific config value", func(t *testing.T) { 67 - handler, err := NewConfigHandler() 68 - if err != nil { 69 - t.Fatalf("Failed to create handler: %v", err) 70 - } 71 - 72 - // Capture stdout 73 - oldStdout := os.Stdout 74 - r, w, _ := os.Pipe() 75 - os.Stdout = w 76 - 77 - err = handler.Get("editor") 78 - if err != nil { 79 - t.Fatalf("Get failed: %v", err) 80 - } 81 - 82 - w.Close() 83 - os.Stdout = oldStdout 84 - 85 - var buf bytes.Buffer 86 - io.Copy(&buf, r) 87 - output := buf.String() 88 89 - if !strings.Contains(output, "editor = vim") { 90 - t.Errorf("Output should contain 'editor = vim', got: %s", output) 91 - } 92 - }) 93 94 - t.Run("Get unknown config key", func(t *testing.T) { 95 - handler, err := NewConfigHandler() 96 - if err != nil { 97 - t.Fatalf("Failed to create handler: %v", err) 98 - } 99 100 - err = handler.Get("nonexistent_key") 101 - if err == nil { 102 - t.Error("Get should fail for unknown key") 103 - } 104 - if !strings.Contains(err.Error(), "unknown config key") { 105 - t.Errorf("Error should mention unknown config key, got: %v", err) 106 - } 107 - }) 108 - } 109 110 - func TestConfigHandlerSet(t *testing.T) { 111 - tempDir, err := os.MkdirTemp("", "noteleaf-config-handler-set-test-*") 112 - if err != nil { 113 - t.Fatalf("Failed to create temp directory: %v", err) 114 - } 115 - defer os.RemoveAll(tempDir) 116 117 - // Set up environment 118 - customConfigPath := filepath.Join(tempDir, "test-config.toml") 119 - originalEnv := os.Getenv("NOTELEAF_CONFIG") 120 - os.Setenv("NOTELEAF_CONFIG", customConfigPath) 121 - defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 122 123 - t.Run("Set string config value", func(t *testing.T) { 124 - handler, err := NewConfigHandler() 125 - if err != nil { 126 - t.Fatalf("Failed to create handler: %v", err) 127 - } 128 129 - // Capture stdout 130 - oldStdout := os.Stdout 131 - r, w, _ := os.Pipe() 132 - os.Stdout = w 133 134 - err = handler.Set("editor", "emacs") 135 - if err != nil { 136 - t.Fatalf("Set failed: %v", err) 137 - } 138 139 - w.Close() 140 - os.Stdout = oldStdout 141 142 - var buf bytes.Buffer 143 - io.Copy(&buf, r) 144 - output := buf.String() 145 146 - if !strings.Contains(output, "Set editor = emacs") { 147 - t.Errorf("Output should confirm setting, got: %s", output) 148 - } 149 150 - // Verify it was actually saved 151 - loadedConfig, err := store.LoadConfig() 152 - if err != nil { 153 - t.Fatalf("Failed to load config: %v", err) 154 - } 155 156 - if loadedConfig.Editor != "emacs" { 157 - t.Errorf("Expected editor 'emacs', got '%s'", loadedConfig.Editor) 158 - } 159 - }) 160 161 - t.Run("Set boolean config value", func(t *testing.T) { 162 - handler, err := NewConfigHandler() 163 - if err != nil { 164 - t.Fatalf("Failed to create handler: %v", err) 165 - } 166 167 - err = handler.Set("auto_archive", "true") 168 - if err != nil { 169 - t.Fatalf("Set failed: %v", err) 170 - } 171 172 - // Verify it was actually saved 173 - loadedConfig, err := store.LoadConfig() 174 - if err != nil { 175 - t.Fatalf("Failed to load config: %v", err) 176 - } 177 178 - if !loadedConfig.AutoArchive { 179 - t.Error("Expected auto_archive to be true") 180 - } 181 - }) 182 183 - t.Run("Set boolean config value with various formats", func(t *testing.T) { 184 - testCases := []struct { 185 - value string 186 - expected bool 187 - }{ 188 - {"true", true}, 189 - {"1", true}, 190 - {"yes", true}, 191 - {"false", false}, 192 - {"0", false}, 193 - {"no", false}, 194 - } 195 196 - for _, tc := range testCases { 197 handler, err := NewConfigHandler() 198 if err != nil { 199 t.Fatalf("Failed to create handler: %v", err) 200 } 201 202 - err = handler.Set("sync_enabled", tc.value) 203 if err != nil { 204 - t.Fatalf("Set failed for value '%s': %v", tc.value, err) 205 } 206 207 loadedConfig, err := store.LoadConfig() ··· 209 t.Fatalf("Failed to load config: %v", err) 210 } 211 212 - if loadedConfig.SyncEnabled != tc.expected { 213 - t.Errorf("For value '%s', expected sync_enabled %v, got %v", tc.value, tc.expected, loadedConfig.SyncEnabled) 214 } 215 - } 216 - }) 217 218 - t.Run("Set unknown config key", func(t *testing.T) { 219 - handler, err := NewConfigHandler() 220 - if err != nil { 221 - t.Fatalf("Failed to create handler: %v", err) 222 - } 223 224 - err = handler.Set("nonexistent_key", "value") 225 - if err == nil { 226 - t.Error("Set should fail for unknown key") 227 - } 228 - if !strings.Contains(err.Error(), "unknown config key") { 229 - t.Errorf("Error should mention unknown config key, got: %v", err) 230 - } 231 - }) 232 - } 233 234 - func TestConfigHandlerPath(t *testing.T) { 235 - tempDir, err := os.MkdirTemp("", "noteleaf-config-handler-path-test-*") 236 - if err != nil { 237 - t.Fatalf("Failed to create temp directory: %v", err) 238 - } 239 - defer os.RemoveAll(tempDir) 240 241 - customConfigPath := filepath.Join(tempDir, "my-config.toml") 242 - originalEnv := os.Getenv("NOTELEAF_CONFIG") 243 - os.Setenv("NOTELEAF_CONFIG", customConfigPath) 244 - defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 245 246 - t.Run("Path returns correct config file path", func(t *testing.T) { 247 - handler, err := NewConfigHandler() 248 - if err != nil { 249 - t.Fatalf("Failed to create handler: %v", err) 250 - } 251 252 - // Capture stdout 253 - oldStdout := os.Stdout 254 - r, w, _ := os.Pipe() 255 - os.Stdout = w 256 257 - err = handler.Path() 258 - if err != nil { 259 - t.Fatalf("Path failed: %v", err) 260 - } 261 262 - w.Close() 263 - os.Stdout = oldStdout 264 265 - var buf bytes.Buffer 266 - io.Copy(&buf, r) 267 - output := strings.TrimSpace(buf.String()) 268 269 - if output != customConfigPath { 270 - t.Errorf("Expected path '%s', got '%s'", customConfigPath, output) 271 - } 272 }) 273 - } 274 275 - func TestConfigHandlerReset(t *testing.T) { 276 - tempDir, err := os.MkdirTemp("", "noteleaf-config-handler-reset-test-*") 277 - if err != nil { 278 - t.Fatalf("Failed to create temp directory: %v", err) 279 - } 280 - defer os.RemoveAll(tempDir) 281 282 - customConfigPath := filepath.Join(tempDir, "test-config.toml") 283 - originalEnv := os.Getenv("NOTELEAF_CONFIG") 284 - os.Setenv("NOTELEAF_CONFIG", customConfigPath) 285 - defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 286 287 - t.Run("Reset restores default config", func(t *testing.T) { 288 - // First, modify the config 289 - config := store.DefaultConfig() 290 - config.ColorScheme = "custom" 291 - config.AutoArchive = true 292 - config.Editor = "emacs" 293 - if err := store.SaveConfig(config); err != nil { 294 - t.Fatalf("Failed to save config: %v", err) 295 - } 296 297 - handler, err := NewConfigHandler() 298 - if err != nil { 299 - t.Fatalf("Failed to create handler: %v", err) 300 - } 301 302 - // Capture stdout 303 - oldStdout := os.Stdout 304 - r, w, _ := os.Pipe() 305 - os.Stdout = w 306 307 - err = handler.Reset() 308 - if err != nil { 309 - t.Fatalf("Reset failed: %v", err) 310 - } 311 312 - w.Close() 313 - os.Stdout = oldStdout 314 315 - var buf bytes.Buffer 316 - io.Copy(&buf, r) 317 - output := buf.String() 318 319 - if !strings.Contains(output, "reset to defaults") { 320 - t.Errorf("Output should confirm reset, got: %s", output) 321 - } 322 323 - // Verify config was reset 324 - loadedConfig, err := store.LoadConfig() 325 - if err != nil { 326 - t.Fatalf("Failed to load config: %v", err) 327 - } 328 329 - defaultConfig := store.DefaultConfig() 330 - if loadedConfig.ColorScheme != defaultConfig.ColorScheme { 331 - t.Errorf("ColorScheme should be reset to default '%s', got '%s'", defaultConfig.ColorScheme, loadedConfig.ColorScheme) 332 - } 333 - if loadedConfig.AutoArchive != defaultConfig.AutoArchive { 334 - t.Errorf("AutoArchive should be reset to default %v, got %v", defaultConfig.AutoArchive, loadedConfig.AutoArchive) 335 - } 336 - if loadedConfig.Editor != defaultConfig.Editor { 337 - t.Errorf("Editor should be reset to default '%s', got '%s'", defaultConfig.Editor, loadedConfig.Editor) 338 - } 339 }) 340 }
··· 8 "strings" 9 "testing" 10 11 + "github.com/stormlightlabs/noteleaf/internal/shared" 12 "github.com/stormlightlabs/noteleaf/internal/store" 13 ) 14 15 + func TestConfigHandler(t *testing.T) { 16 + t.Run("Get", func(t *testing.T) { 17 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-handler-get-test-*", t) 18 + defer cleanup() 19 20 + customConfigPath := filepath.Join(tempDir, "test-config.toml") 21 + originalEnv := os.Getenv("NOTELEAF_CONFIG") 22 + os.Setenv("NOTELEAF_CONFIG", customConfigPath) 23 + defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 24 25 + config := store.DefaultConfig() 26 + config.ColorScheme = "test-scheme" 27 + config.Editor = "vim" 28 + if err := store.SaveConfig(config); err != nil { 29 + t.Fatalf("Failed to save config: %v", err) 30 } 31 32 + t.Run("Get all config values", func(t *testing.T) { 33 + handler, err := NewConfigHandler() 34 + if err != nil { 35 + t.Fatalf("Failed to create handler: %v", err) 36 + } 37 38 + oldStdout := os.Stdout 39 + r, w, _ := os.Pipe() 40 + os.Stdout = w 41 42 + err = handler.Get("") 43 + if err != nil { 44 + t.Fatalf("Get failed: %v", err) 45 + } 46 47 + w.Close() 48 + os.Stdout = oldStdout 49 50 + var buf bytes.Buffer 51 + io.Copy(&buf, r) 52 + output := buf.String() 53 54 + if !strings.Contains(output, "color_scheme") { 55 + t.Error("Output should contain color_scheme") 56 + } 57 + if !strings.Contains(output, "test-scheme") { 58 + t.Error("Output should contain test-scheme value") 59 + } 60 + }) 61 62 + t.Run("Get specific config value", func(t *testing.T) { 63 + handler, err := NewConfigHandler() 64 + if err != nil { 65 + t.Fatalf("Failed to create handler: %v", err) 66 + } 67 68 + oldStdout := os.Stdout 69 + r, w, _ := os.Pipe() 70 + os.Stdout = w 71 72 + err = handler.Get("editor") 73 + if err != nil { 74 + t.Fatalf("Get failed: %v", err) 75 + } 76 77 + w.Close() 78 + os.Stdout = oldStdout 79 80 + var buf bytes.Buffer 81 + io.Copy(&buf, r) 82 + output := buf.String() 83 84 + if !strings.Contains(output, "editor = vim") { 85 + t.Errorf("Output should contain 'editor = vim', got: %s", output) 86 + } 87 + }) 88 89 + t.Run("Get unknown config key", func(t *testing.T) { 90 + handler, err := NewConfigHandler() 91 + if err != nil { 92 + t.Fatalf("Failed to create handler: %v", err) 93 + } 94 95 + err = handler.Get("nonexistent_key") 96 + if err == nil { 97 + t.Error("Get should fail for unknown key") 98 + } 99 + if !strings.Contains(err.Error(), "unknown config key") { 100 + t.Errorf("Error should mention unknown config key, got: %v", err) 101 + } 102 + }) 103 + }) 104 105 + t.Run("Set", func(t *testing.T) { 106 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-handler-set-test-*", t) 107 + defer cleanup() 108 109 + customConfigPath := filepath.Join(tempDir, "test-config.toml") 110 + originalEnv := os.Getenv("NOTELEAF_CONFIG") 111 + os.Setenv("NOTELEAF_CONFIG", customConfigPath) 112 + defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 113 114 + t.Run("Set string config value", func(t *testing.T) { 115 + handler, err := NewConfigHandler() 116 + if err != nil { 117 + t.Fatalf("Failed to create handler: %v", err) 118 + } 119 120 + oldStdout := os.Stdout 121 + r, w, _ := os.Pipe() 122 + os.Stdout = w 123 124 + err = handler.Set("editor", "emacs") 125 + if err != nil { 126 + t.Fatalf("Set failed: %v", err) 127 + } 128 129 + w.Close() 130 + os.Stdout = oldStdout 131 132 + var buf bytes.Buffer 133 + io.Copy(&buf, r) 134 + output := buf.String() 135 136 + if !strings.Contains(output, "Set editor = emacs") { 137 + t.Errorf("Output should confirm setting, got: %s", output) 138 + } 139 140 + loadedConfig, err := store.LoadConfig() 141 + if err != nil { 142 + t.Fatalf("Failed to load config: %v", err) 143 + } 144 145 + if loadedConfig.Editor != "emacs" { 146 + t.Errorf("Expected editor 'emacs', got '%s'", loadedConfig.Editor) 147 + } 148 + }) 149 150 + t.Run("Set boolean config value", func(t *testing.T) { 151 handler, err := NewConfigHandler() 152 if err != nil { 153 t.Fatalf("Failed to create handler: %v", err) 154 } 155 156 + err = handler.Set("auto_archive", "true") 157 if err != nil { 158 + t.Fatalf("Set failed: %v", err) 159 } 160 161 loadedConfig, err := store.LoadConfig() ··· 163 t.Fatalf("Failed to load config: %v", err) 164 } 165 166 + if !loadedConfig.AutoArchive { 167 + t.Error("Expected auto_archive to be true") 168 } 169 + }) 170 171 + t.Run("Set boolean config value with various formats", func(t *testing.T) { 172 + tc := []struct { 173 + value string 174 + expected bool 175 + }{ 176 + {"true", true}, 177 + {"1", true}, 178 + {"yes", true}, 179 + {"false", false}, 180 + {"0", false}, 181 + {"no", false}, 182 + } 183 184 + for _, tt := range tc { 185 + handler, err := NewConfigHandler() 186 + if err != nil { 187 + t.Fatalf("Failed to create handler: %v", err) 188 + } 189 190 + err = handler.Set("sync_enabled", tt.value) 191 + if err != nil { 192 + t.Fatalf("Set failed for value '%s': %v", tt.value, err) 193 + } 194 195 + loadedConfig, err := store.LoadConfig() 196 + if err != nil { 197 + t.Fatalf("Failed to load config: %v", err) 198 + } 199 200 + if loadedConfig.SyncEnabled != tt.expected { 201 + t.Errorf("For value '%s', expected sync_enabled %v, got %v", tt.value, tt.expected, loadedConfig.SyncEnabled) 202 + } 203 + } 204 + }) 205 206 + t.Run("Set unknown config key", func(t *testing.T) { 207 + handler, err := NewConfigHandler() 208 + if err != nil { 209 + t.Fatalf("Failed to create handler: %v", err) 210 + } 211 212 + err = handler.Set("nonexistent_key", "value") 213 + if err == nil { 214 + t.Error("Set should fail for unknown key") 215 + } 216 + if !strings.Contains(err.Error(), "unknown config key") { 217 + t.Errorf("Error should mention unknown config key, got: %v", err) 218 + } 219 + }) 220 + }) 221 + t.Run("Path", func(t *testing.T) { 222 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-handler-path-test-*", t) 223 + defer cleanup() 224 225 + customConfigPath := filepath.Join(tempDir, "my-config.toml") 226 + originalEnv := os.Getenv("NOTELEAF_CONFIG") 227 + os.Setenv("NOTELEAF_CONFIG", customConfigPath) 228 + defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 229 230 + t.Run("Path returns correct config file path", func(t *testing.T) { 231 + handler, err := NewConfigHandler() 232 + if err != nil { 233 + t.Fatalf("Failed to create handler: %v", err) 234 + } 235 236 + oldStdout := os.Stdout 237 + r, w, _ := os.Pipe() 238 + os.Stdout = w 239 + 240 + err = handler.Path() 241 + if err != nil { 242 + t.Fatalf("Path failed: %v", err) 243 + } 244 + 245 + w.Close() 246 + os.Stdout = oldStdout 247 + 248 + var buf bytes.Buffer 249 + io.Copy(&buf, r) 250 + output := strings.TrimSpace(buf.String()) 251 + 252 + if output != customConfigPath { 253 + t.Errorf("Expected path '%s', got '%s'", customConfigPath, output) 254 + } 255 + }) 256 }) 257 258 + t.Run("Reset", func(t *testing.T) { 259 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-handler-reset-test-*", t) 260 + defer cleanup() 261 262 + customConfigPath := filepath.Join(tempDir, "test-config.toml") 263 + originalEnv := os.Getenv("NOTELEAF_CONFIG") 264 + os.Setenv("NOTELEAF_CONFIG", customConfigPath) 265 + defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 266 267 + t.Run("Reset restores default config", func(t *testing.T) { 268 + config := store.DefaultConfig() 269 + config.ColorScheme = "custom" 270 + config.AutoArchive = true 271 + config.Editor = "emacs" 272 + if err := store.SaveConfig(config); err != nil { 273 + t.Fatalf("Failed to save config: %v", err) 274 + } 275 276 + handler, err := NewConfigHandler() 277 + if err != nil { 278 + t.Fatalf("Failed to create handler: %v", err) 279 + } 280 281 + oldStdout := os.Stdout 282 + r, w, _ := os.Pipe() 283 + os.Stdout = w 284 285 + err = handler.Reset() 286 + if err != nil { 287 + t.Fatalf("Reset failed: %v", err) 288 + } 289 290 + w.Close() 291 + os.Stdout = oldStdout 292 293 + var buf bytes.Buffer 294 + io.Copy(&buf, r) 295 + output := buf.String() 296 297 + if !strings.Contains(output, "reset to defaults") { 298 + t.Errorf("Output should confirm reset, got: %s", output) 299 + } 300 301 + loadedConfig, err := store.LoadConfig() 302 + if err != nil { 303 + t.Fatalf("Failed to load config: %v", err) 304 + } 305 306 + defaultConfig := store.DefaultConfig() 307 + if loadedConfig.ColorScheme != defaultConfig.ColorScheme { 308 + t.Errorf("ColorScheme should be reset to default '%s', got '%s'", defaultConfig.ColorScheme, loadedConfig.ColorScheme) 309 + } 310 + if loadedConfig.AutoArchive != defaultConfig.AutoArchive { 311 + t.Errorf("AutoArchive should be reset to default %v, got %v", defaultConfig.AutoArchive, loadedConfig.AutoArchive) 312 + } 313 + if loadedConfig.Editor != defaultConfig.Editor { 314 + t.Errorf("Editor should be reset to default '%s', got '%s'", defaultConfig.Editor, loadedConfig.Editor) 315 + } 316 + }) 317 }) 318 }
+37 -38
internal/handlers/notes_test.go
··· 11 "time" 12 13 "github.com/stormlightlabs/noteleaf/internal/models" 14 "github.com/stormlightlabs/noteleaf/internal/store" 15 ) 16 - 17 - // setupNoteTest removed - use NewHandlerTestSuite(t) instead 18 19 func createTestMarkdownFile(t *testing.T, dir, filename, content string) string { 20 filePath := filepath.Join(dir, filename) ··· 37 t.Run("New", func(t *testing.T) { 38 t.Run("creates handler successfully", func(t *testing.T) { 39 testHandler, err := NewNoteHandler() 40 - Expect.AssertNoError(t, err, "NewNoteHandler should succeed") 41 if testHandler == nil { 42 t.Fatal("Handler should not be nil") 43 } ··· 68 envHelper.UnsetEnv("NOTELEAF_DATA_DIR") 69 70 _, err := NewNoteHandler() 71 - Expect.AssertError(t, err, "failed to initialize database", "NewNoteHandler should fail when database initialization fails") 72 }) 73 }) 74 ··· 77 78 t.Run("creates note from title only", func(t *testing.T) { 79 err := handler.Create(ctx, "Test Note 1", "", "", false) 80 - Expect.AssertNoError(t, err, "Create should succeed") 81 }) 82 83 t.Run("creates note from title and content", func(t *testing.T) { 84 err := handler.Create(ctx, "Test Note 2", "This is test content", "", false) 85 - Expect.AssertNoError(t, err, "Create should succeed") 86 }) 87 88 t.Run("creates note from markdown file", func(t *testing.T) { ··· 93 filePath := createTestMarkdownFile(t, suite.TempDir(), "test.md", content) 94 95 err := handler.Create(ctx, "", "", filePath, false) 96 - Expect.AssertNoError(t, err, "Create from file should succeed") 97 }) 98 99 t.Run("handles non-existent file", func(t *testing.T) { 100 err := handler.Create(ctx, "", "", "/non/existent/file.md", false) 101 - Expect.AssertError(t, err, "", "Create should fail with non-existent file") 102 }) 103 }) 104 ··· 107 108 t.Run("handles non-existent note", func(t *testing.T) { 109 err := handler.Edit(ctx, 999) 110 - Expect.AssertError(t, err, "failed to get note", "Edit should fail with non-existent note ID") 111 }) 112 113 t.Run("handles no editor configured", func(t *testing.T) { ··· 118 envHelper.SetEnv("PATH", "") 119 120 err := handler.Edit(ctx, 1) 121 - Expect.AssertError(t, err, "failed to open editor", "Edit should fail when no editor is configured") 122 }) 123 124 t.Run("handles database connection error", func(t *testing.T) { ··· 126 defer func() { 127 var err error 128 handler.db, err = store.NewDatabase() 129 - Expect.AssertNoError(t, err, "Failed to reconnect to database") 130 }() 131 132 err := handler.Edit(ctx, 1) 133 - Expect.AssertError(t, err, "failed to get note", "Edit should fail when database is closed") 134 }) 135 136 t.Run("handles temp file creation error", func(t *testing.T) { 137 testHandler, err := NewNoteHandler() 138 - Expect.AssertNoError(t, err, "Failed to create test handler") 139 defer testHandler.Close() 140 141 err = testHandler.Create(ctx, "Temp File Test Note", "Test content", "", false) 142 - Expect.AssertNoError(t, err, "Failed to create test note") 143 144 envHelper := NewEnvironmentTestHelper() 145 defer envHelper.RestoreEnv() 146 envHelper.SetEnv("TMPDIR", "/non/existent/path") 147 148 err = testHandler.Edit(ctx, 1) 149 - Expect.AssertError(t, err, "failed to create temporary file", "Edit should fail when temp file creation fails") 150 }) 151 152 t.Run("handles editor failure", func(t *testing.T) { 153 testHandler, err := NewNoteHandler() 154 - Expect.AssertNoError(t, err, "Failed to create test handler") 155 defer testHandler.Close() 156 157 err = testHandler.Create(ctx, "Editor Failure Test Note", "Test content", "", false) 158 - Expect.AssertNoError(t, err, "Failed to create test note") 159 160 mockEditor := NewMockEditor().WithFailure("editor process failed") 161 testHandler.openInEditorFunc = mockEditor.GetEditorFunc() 162 163 err = testHandler.Edit(ctx, 1) 164 - Expect.AssertError(t, err, "failed to open editor", "Edit should fail when editor fails") 165 }) 166 167 t.Run("handles temp file write error", func(t *testing.T) { ··· 172 handler.openInEditorFunc = mockEditor.GetEditorFunc() 173 174 err := handler.Edit(ctx, 1) 175 - Expect.AssertError(t, err, "", "Edit should handle temp file write issues") 176 }) 177 178 t.Run("handles file read error after editing", func(t *testing.T) { 179 testHandler, err := NewNoteHandler() 180 - Expect.AssertNoError(t, err, "Failed to create test handler") 181 defer testHandler.Close() 182 183 err = testHandler.Create(ctx, "File Read Error Test Note", "Test content", "", false) 184 - Expect.AssertNoError(t, err, "Failed to create test note") 185 186 mockEditor := NewMockEditor().WithFileDeleted() 187 testHandler.openInEditorFunc = mockEditor.GetEditorFunc() 188 189 err = testHandler.Edit(ctx, 1) 190 - Expect.AssertError(t, err, "failed to read edited content", "Edit should fail when temp file is deleted") 191 }) 192 193 t.Run("handles database update error", func(t *testing.T) { ··· 203 handler.openInEditorFunc = mockEditor.GetEditorFunc() 204 205 err := handler.Edit(ctx, id) 206 - Expect.AssertError(t, err, "failed to get note", "Edit should fail when database is corrupted") 207 }) 208 209 t.Run("handles validation error - corrupted note content", func(t *testing.T) { ··· 282 handler.openInEditorFunc = mockEditor.GetEditorFunc() 283 284 err := handler.Edit(ctx, id) 285 - Expect.AssertNoError(t, err, "Edit should succeed") 286 }) 287 288 t.Run("Edit Errors", func(t *testing.T) { ··· 351 handler.openInEditorFunc = mockEditor.GetEditorFunc() 352 err := handler.Edit(ctx, noteID) 353 354 - Expect.AssertNoError(t, err, "Edit should succeed") 355 AssertExists(t, handler.repos.Notes.Get, noteID, "note") 356 }) 357 ··· 369 handler.openInEditorFunc = mockEditor.GetEditorFunc() 370 371 err := handler.Edit(ctx, noteID) 372 - Expect.AssertNoError(t, err, "Edit should succeed even with no changes") 373 }) 374 375 t.Run("handles content without title", func(t *testing.T) { ··· 382 handler.openInEditorFunc = mockEditor.GetEditorFunc() 383 384 err := handler.Edit(ctx, noteID) 385 - Expect.AssertNoError(t, err, "Edit should succeed without title") 386 }) 387 }) 388 }) ··· 701 handler.openInEditorFunc = mockEditor.GetEditorFunc() 702 703 err := handler.createInteractive(ctx) 704 - Expect.AssertNoError(t, err, "createInteractive should succeed") 705 }) 706 707 t.Run("handles cancelled note creation", func(t *testing.T) { ··· 710 handler.openInEditorFunc = mockEditor.GetEditorFunc() 711 712 err := handler.createInteractive(ctx) 713 - Expect.AssertNoError(t, err, "createInteractive should succeed even when cancelled") 714 }) 715 716 t.Run("handles editor error", func(t *testing.T) { ··· 719 handler.openInEditorFunc = mockEditor.GetEditorFunc() 720 721 err := handler.createInteractive(ctx) 722 - Expect.AssertError(t, err, "failed to open editor", "createInteractive should fail when editor fails") 723 }) 724 725 t.Run("handles no editor configured", func(t *testing.T) { ··· 731 envHelper.SetEnv("PATH", "") 732 733 err := handler.createInteractive(ctx) 734 - Expect.AssertError(t, err, "no editor configured", "createInteractive should fail when no editor is configured") 735 }) 736 737 t.Run("handles file read error after editing", func(t *testing.T) { ··· 740 handler.openInEditorFunc = mockEditor.GetEditorFunc() 741 742 err := handler.createInteractive(ctx) 743 - Expect.AssertError(t, err, "failed to read edited content", "createInteractive should fail when temp file is deleted") 744 }) 745 }) 746 ··· 750 t.Run("creates note successfully without editor prompt", func(t *testing.T) { 751 handler := NewHandlerTestHelper(t) 752 err := handler.CreateWithOptions(ctx, "Test Note", "Test content", "", false, false) 753 - Expect.AssertNoError(t, err, "CreateWithOptions should succeed") 754 }) 755 756 t.Run("creates note successfully with editor prompt disabled", func(t *testing.T) { 757 handler := NewHandlerTestHelper(t) 758 err := handler.CreateWithOptions(ctx, "Another Test Note", "More content", "", false, false) 759 - Expect.AssertNoError(t, err, "CreateWithOptions should succeed") 760 }) 761 762 t.Run("handles database error during creation", func(t *testing.T) { ··· 765 cancel() 766 767 err := handler.CreateWithOptions(cancelCtx, "Test Note", "Test content", "", false, false) 768 - Expect.AssertError(t, err, "failed to create note", "CreateWithOptions should fail with cancelled context") 769 }) 770 771 t.Run("creates note with empty content", func(t *testing.T) { 772 handler := NewHandlerTestHelper(t) 773 err := handler.CreateWithOptions(ctx, "Empty Content Note", "", "", false, false) 774 - Expect.AssertNoError(t, err, "CreateWithOptions should succeed with empty content") 775 }) 776 777 t.Run("creates note with empty title", func(t *testing.T) { 778 handler := NewHandlerTestHelper(t) 779 err := handler.CreateWithOptions(ctx, "", "Content without title", "", false, false) 780 - Expect.AssertNoError(t, err, "CreateWithOptions should succeed with empty title") 781 }) 782 783 t.Run("handles editor prompt with no editor available", func(t *testing.T) { ··· 789 envHelper.SetEnv("PATH", "") 790 791 err := handler.CreateWithOptions(ctx, "Test Note", "Test content", "", false, true) 792 - Expect.AssertNoError(t, err, "CreateWithOptions should succeed even when no editor is available") 793 }) 794 }) 795
··· 11 "time" 12 13 "github.com/stormlightlabs/noteleaf/internal/models" 14 + "github.com/stormlightlabs/noteleaf/internal/shared" 15 "github.com/stormlightlabs/noteleaf/internal/store" 16 ) 17 18 func createTestMarkdownFile(t *testing.T, dir, filename, content string) string { 19 filePath := filepath.Join(dir, filename) ··· 36 t.Run("New", func(t *testing.T) { 37 t.Run("creates handler successfully", func(t *testing.T) { 38 testHandler, err := NewNoteHandler() 39 + shared.AssertNoError(t, err, "NewNoteHandler should succeed") 40 if testHandler == nil { 41 t.Fatal("Handler should not be nil") 42 } ··· 67 envHelper.UnsetEnv("NOTELEAF_DATA_DIR") 68 69 _, err := NewNoteHandler() 70 + shared.AssertErrorContains(t, err, "failed to initialize database", "NewNoteHandler should fail when database initialization fails") 71 }) 72 }) 73 ··· 76 77 t.Run("creates note from title only", func(t *testing.T) { 78 err := handler.Create(ctx, "Test Note 1", "", "", false) 79 + shared.AssertNoError(t, err, "Create should succeed") 80 }) 81 82 t.Run("creates note from title and content", func(t *testing.T) { 83 err := handler.Create(ctx, "Test Note 2", "This is test content", "", false) 84 + shared.AssertNoError(t, err, "Create should succeed") 85 }) 86 87 t.Run("creates note from markdown file", func(t *testing.T) { ··· 92 filePath := createTestMarkdownFile(t, suite.TempDir(), "test.md", content) 93 94 err := handler.Create(ctx, "", "", filePath, false) 95 + shared.AssertNoError(t, err, "Create from file should succeed") 96 }) 97 98 t.Run("handles non-existent file", func(t *testing.T) { 99 err := handler.Create(ctx, "", "", "/non/existent/file.md", false) 100 + shared.AssertErrorContains(t, err, "", "Create should fail with non-existent file") 101 }) 102 }) 103 ··· 106 107 t.Run("handles non-existent note", func(t *testing.T) { 108 err := handler.Edit(ctx, 999) 109 + shared.AssertErrorContains(t, err, "failed to get note", "Edit should fail with non-existent note ID") 110 }) 111 112 t.Run("handles no editor configured", func(t *testing.T) { ··· 117 envHelper.SetEnv("PATH", "") 118 119 err := handler.Edit(ctx, 1) 120 + shared.AssertErrorContains(t, err, "failed to open editor", "Edit should fail when no editor is configured") 121 }) 122 123 t.Run("handles database connection error", func(t *testing.T) { ··· 125 defer func() { 126 var err error 127 handler.db, err = store.NewDatabase() 128 + shared.AssertNoError(t, err, "Failed to reconnect to database") 129 }() 130 131 err := handler.Edit(ctx, 1) 132 + shared.AssertErrorContains(t, err, "failed to get note", "Edit should fail when database is closed") 133 }) 134 135 t.Run("handles temp file creation error", func(t *testing.T) { 136 testHandler, err := NewNoteHandler() 137 + shared.AssertNoError(t, err, "Failed to create test handler") 138 defer testHandler.Close() 139 140 err = testHandler.Create(ctx, "Temp File Test Note", "Test content", "", false) 141 + shared.AssertNoError(t, err, "Failed to create test note") 142 143 envHelper := NewEnvironmentTestHelper() 144 defer envHelper.RestoreEnv() 145 envHelper.SetEnv("TMPDIR", "/non/existent/path") 146 147 err = testHandler.Edit(ctx, 1) 148 + shared.AssertErrorContains(t, err, "failed to create temporary file", "Edit should fail when temp file creation fails") 149 }) 150 151 t.Run("handles editor failure", func(t *testing.T) { 152 testHandler, err := NewNoteHandler() 153 + shared.AssertNoError(t, err, "Failed to create test handler") 154 defer testHandler.Close() 155 156 err = testHandler.Create(ctx, "Editor Failure Test Note", "Test content", "", false) 157 + shared.AssertNoError(t, err, "Failed to create test note") 158 159 mockEditor := NewMockEditor().WithFailure("editor process failed") 160 testHandler.openInEditorFunc = mockEditor.GetEditorFunc() 161 162 err = testHandler.Edit(ctx, 1) 163 + shared.AssertErrorContains(t, err, "failed to open editor", "Edit should fail when editor fails") 164 }) 165 166 t.Run("handles temp file write error", func(t *testing.T) { ··· 171 handler.openInEditorFunc = mockEditor.GetEditorFunc() 172 173 err := handler.Edit(ctx, 1) 174 + shared.AssertErrorContains(t, err, "", "Edit should handle temp file write issues") 175 }) 176 177 t.Run("handles file read error after editing", func(t *testing.T) { 178 testHandler, err := NewNoteHandler() 179 + shared.AssertNoError(t, err, "Failed to create test handler") 180 defer testHandler.Close() 181 182 err = testHandler.Create(ctx, "File Read Error Test Note", "Test content", "", false) 183 + shared.AssertNoError(t, err, "Failed to create test note") 184 185 mockEditor := NewMockEditor().WithFileDeleted() 186 testHandler.openInEditorFunc = mockEditor.GetEditorFunc() 187 188 err = testHandler.Edit(ctx, 1) 189 + shared.AssertErrorContains(t, err, "failed to read edited content", "Edit should fail when temp file is deleted") 190 }) 191 192 t.Run("handles database update error", func(t *testing.T) { ··· 202 handler.openInEditorFunc = mockEditor.GetEditorFunc() 203 204 err := handler.Edit(ctx, id) 205 + shared.AssertErrorContains(t, err, "failed to get note", "Edit should fail when database is corrupted") 206 }) 207 208 t.Run("handles validation error - corrupted note content", func(t *testing.T) { ··· 281 handler.openInEditorFunc = mockEditor.GetEditorFunc() 282 283 err := handler.Edit(ctx, id) 284 + shared.AssertNoError(t, err, "Edit should succeed") 285 }) 286 287 t.Run("Edit Errors", func(t *testing.T) { ··· 350 handler.openInEditorFunc = mockEditor.GetEditorFunc() 351 err := handler.Edit(ctx, noteID) 352 353 + shared.AssertNoError(t, err, "Edit should succeed") 354 AssertExists(t, handler.repos.Notes.Get, noteID, "note") 355 }) 356 ··· 368 handler.openInEditorFunc = mockEditor.GetEditorFunc() 369 370 err := handler.Edit(ctx, noteID) 371 + shared.AssertNoError(t, err, "Edit should succeed even with no changes") 372 }) 373 374 t.Run("handles content without title", func(t *testing.T) { ··· 381 handler.openInEditorFunc = mockEditor.GetEditorFunc() 382 383 err := handler.Edit(ctx, noteID) 384 + shared.AssertNoError(t, err, "Edit should succeed without title") 385 }) 386 }) 387 }) ··· 700 handler.openInEditorFunc = mockEditor.GetEditorFunc() 701 702 err := handler.createInteractive(ctx) 703 + shared.AssertNoError(t, err, "createInteractive should succeed") 704 }) 705 706 t.Run("handles cancelled note creation", func(t *testing.T) { ··· 709 handler.openInEditorFunc = mockEditor.GetEditorFunc() 710 711 err := handler.createInteractive(ctx) 712 + shared.AssertNoError(t, err, "createInteractive should succeed even when cancelled") 713 }) 714 715 t.Run("handles editor error", func(t *testing.T) { ··· 718 handler.openInEditorFunc = mockEditor.GetEditorFunc() 719 720 err := handler.createInteractive(ctx) 721 + shared.AssertErrorContains(t, err, "failed to open editor", "createInteractive should fail when editor fails") 722 }) 723 724 t.Run("handles no editor configured", func(t *testing.T) { ··· 730 envHelper.SetEnv("PATH", "") 731 732 err := handler.createInteractive(ctx) 733 + shared.AssertErrorContains(t, err, "no editor configured", "createInteractive should fail when no editor is configured") 734 }) 735 736 t.Run("handles file read error after editing", func(t *testing.T) { ··· 739 handler.openInEditorFunc = mockEditor.GetEditorFunc() 740 741 err := handler.createInteractive(ctx) 742 + shared.AssertErrorContains(t, err, "failed to read edited content", "createInteractive should fail when temp file is deleted") 743 }) 744 }) 745 ··· 749 t.Run("creates note successfully without editor prompt", func(t *testing.T) { 750 handler := NewHandlerTestHelper(t) 751 err := handler.CreateWithOptions(ctx, "Test Note", "Test content", "", false, false) 752 + shared.AssertNoError(t, err, "CreateWithOptions should succeed") 753 }) 754 755 t.Run("creates note successfully with editor prompt disabled", func(t *testing.T) { 756 handler := NewHandlerTestHelper(t) 757 err := handler.CreateWithOptions(ctx, "Another Test Note", "More content", "", false, false) 758 + shared.AssertNoError(t, err, "CreateWithOptions should succeed") 759 }) 760 761 t.Run("handles database error during creation", func(t *testing.T) { ··· 764 cancel() 765 766 err := handler.CreateWithOptions(cancelCtx, "Test Note", "Test content", "", false, false) 767 + shared.AssertErrorContains(t, err, "failed to create note", "CreateWithOptions should fail with cancelled context") 768 }) 769 770 t.Run("creates note with empty content", func(t *testing.T) { 771 handler := NewHandlerTestHelper(t) 772 err := handler.CreateWithOptions(ctx, "Empty Content Note", "", "", false, false) 773 + shared.AssertNoError(t, err, "CreateWithOptions should succeed with empty content") 774 }) 775 776 t.Run("creates note with empty title", func(t *testing.T) { 777 handler := NewHandlerTestHelper(t) 778 err := handler.CreateWithOptions(ctx, "", "Content without title", "", false, false) 779 + shared.AssertNoError(t, err, "CreateWithOptions should succeed with empty title") 780 }) 781 782 t.Run("handles editor prompt with no editor available", func(t *testing.T) { ··· 788 envHelper.SetEnv("PATH", "") 789 790 err := handler.CreateWithOptions(ctx, "Test Note", "Test content", "", false, true) 791 + shared.AssertNoError(t, err, "CreateWithOptions should succeed even when no editor is available") 792 }) 793 }) 794
-2
internal/handlers/seed_test.go
··· 10 "github.com/stormlightlabs/noteleaf/internal/store" 11 ) 12 13 - // setupSeedTest removed - use NewHandlerTestSuite(t) instead 14 - 15 func countRecords(t *testing.T, db *store.Database, table string) int { 16 t.Helper() 17
··· 10 "github.com/stormlightlabs/noteleaf/internal/store" 11 ) 12 13 func countRecords(t *testing.T, db *store.Database, table string) int { 14 t.Helper() 15
+5 -5
internal/handlers/tasks_test.go
··· 13 14 "github.com/google/uuid" 15 "github.com/stormlightlabs/noteleaf/internal/models" 16 - "github.com/stormlightlabs/noteleaf/internal/repo" 17 "github.com/stormlightlabs/noteleaf/internal/ui" 18 ) 19 ··· 78 t.Run("creates task successfully", func(t *testing.T) { 79 desc := "Buy groceries and cook dinner" 80 err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", []string{}) 81 - repo.AssertNoError(t, err, "CreateTask should succeed") 82 83 tasks, err := handler.repos.Tasks.GetPending(ctx) 84 - repo.AssertNoError(t, err, "Failed to get pending tasks") 85 86 if len(tasks) != 1 { 87 t.Errorf("Expected 1 task, got %d", len(tasks)) ··· 105 t.Run("fails with empty description", func(t *testing.T) { 106 desc := "" 107 err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", []string{}) 108 - repo.AssertError(t, err, "Expected error for empty description") 109 - repo.AssertContains(t, err.Error(), "task description required", "Error message should mention required description") 110 }) 111 112 t.Run("creates task with flags", func(t *testing.T) {
··· 13 14 "github.com/google/uuid" 15 "github.com/stormlightlabs/noteleaf/internal/models" 16 + "github.com/stormlightlabs/noteleaf/internal/shared" 17 "github.com/stormlightlabs/noteleaf/internal/ui" 18 ) 19 ··· 78 t.Run("creates task successfully", func(t *testing.T) { 79 desc := "Buy groceries and cook dinner" 80 err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", []string{}) 81 + shared.AssertNoError(t, err, "CreateTask should succeed") 82 83 tasks, err := handler.repos.Tasks.GetPending(ctx) 84 + shared.AssertNoError(t, err, "Failed to get pending tasks") 85 86 if len(tasks) != 1 { 87 t.Errorf("Expected 1 task, got %d", len(tasks)) ··· 105 t.Run("fails with empty description", func(t *testing.T) { 106 desc := "" 107 err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", []string{}) 108 + shared.AssertError(t, err, "Expected error for empty description") 109 + shared.AssertContains(t, err.Error(), "task description required", "Error message should mention required description") 110 }) 111 112 t.Run("creates task with flags", func(t *testing.T) {
+7 -206
internal/handlers/test_utilities.go
··· 2 3 import ( 4 "context" 5 - "encoding/json" 6 "fmt" 7 "io" 8 - "net/http" 9 - "net/http/httptest" 10 "os" 11 "path/filepath" 12 "strconv" ··· 24 "github.com/stormlightlabs/noteleaf/internal/ui" 25 ) 26 27 - // HandlerTestHelper wraps NoteHandler with test-specific functionality 28 // 29 - // Uses HandlerTestSuite internally to avoid code duplication 30 type HandlerTestHelper struct { 31 *NoteHandler 32 suite *HandlerTestSuite 33 } 34 35 - // NewHandlerTestHelper creates a NoteHandler with isolated test database 36 func NewHandlerTestHelper(t *testing.T) *HandlerTestHelper { 37 suite := NewHandlerTestSuite(t) 38 ··· 126 return me 127 } 128 129 - // GetEditorFunc returns the editor function for use with NoteHandler 130 func (me *MockEditor) GetEditorFunc() editorFunc { 131 return func(editor, filePath string) error { 132 if me.shouldFail { ··· 192 dth.handler.db = db 193 } 194 195 - // AssertionHelpers provides test assertion utilities 196 - type AssertionHelpers struct{} 197 - 198 - // AssertError checks that an error occurred and optionally contains expected text 199 - func (ah AssertionHelpers) AssertError(t *testing.T, err error, expectedSubstring string, msg string) { 200 - t.Helper() 201 - if err == nil { 202 - t.Errorf("%s: expected error but got none", msg) 203 - return 204 - } 205 - if expectedSubstring != "" && !containsString(err.Error(), expectedSubstring) { 206 - t.Errorf("%s: expected error containing %q, got: %v", msg, expectedSubstring, err) 207 - } 208 - } 209 - 210 - // AssertNoError checks that no error occurred 211 - func (ah AssertionHelpers) AssertNoError(t *testing.T, err error, msg string) { 212 - t.Helper() 213 - if err != nil { 214 - t.Errorf("%s: unexpected error: %v", msg, err) 215 - } 216 - } 217 - 218 // EnvironmentTestHelper provides environment manipulation utilities for testing 219 - // 220 - // Use this for tests requiring fine-grained environment control beyond HandlerTestSuite. 221 - // Examples: testing missing EDITOR, invalid PATH, corrupt TMPDIR, etc. 222 type EnvironmentTestHelper struct { 223 originalVars map[string]string 224 } ··· 281 return tempDir, nil 282 } 283 284 - // Helper function to check if string contains substring (case-insensitive) 285 - func containsString(haystack, needle string) bool { 286 - if needle == "" { 287 - return true 288 - } 289 - return len(haystack) >= len(needle) && 290 - haystack[len(haystack)-len(needle):] == needle || 291 - haystack[:len(needle)] == needle || 292 - (len(haystack) > len(needle) && 293 - func() bool { 294 - for i := 1; i <= len(haystack)-len(needle); i++ { 295 - if haystack[i:i+len(needle)] == needle { 296 - return true 297 - } 298 - } 299 - return false 300 - }()) 301 - } 302 - 303 - // ArticleTestHelper wraps ArticleHandler with test-specific functionality 304 type ArticleTestHelper struct { 305 *ArticleHandler 306 suite *HandlerTestSuite 307 } 308 309 - // NewArticleTestHelper creates an ArticleHandler with isolated test database 310 func NewArticleTestHelper(t *testing.T) *ArticleTestHelper { 311 suite := NewHandlerTestSuite(t) 312 ··· 373 } 374 } 375 376 - var Expect = AssertionHelpers{} 377 - 378 - // HTTPMockServer provides utilities for mocking HTTP services in tests 379 - type HTTPMockServer struct { 380 - server *httptest.Server 381 - requests []*http.Request 382 - } 383 - 384 - // NewMockServer creates a new mock HTTP server 385 - func NewMockServer() *HTTPMockServer { 386 - mock := &HTTPMockServer{ 387 - requests: make([]*http.Request, 0), 388 - } 389 - return mock 390 - } 391 - 392 - // WithHandler sets up the mock server with a custom handler 393 - func (m *HTTPMockServer) WithHandler(handler http.HandlerFunc) *HTTPMockServer { 394 - m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 395 - m.requests = append(m.requests, r) 396 - handler(w, r) 397 - })) 398 - return m 399 - } 400 - 401 - // URL returns the mock server URL 402 - func (m *HTTPMockServer) URL() string { 403 - if m.server == nil { 404 - panic("mock server not initialized - call WithHandler first") 405 - } 406 - return m.server.URL 407 - } 408 - 409 - // Close closes the mock server 410 - func (m *HTTPMockServer) Close() { 411 - if m.server != nil { 412 - m.server.Close() 413 - } 414 - } 415 - 416 - // GetRequests returns all recorded HTTP requests 417 - func (m *HTTPMockServer) GetRequests() []*http.Request { 418 - return m.requests 419 - } 420 - 421 - // GetLastRequest returns the last recorded HTTP request 422 - func (m *HTTPMockServer) GetLastRequest() *http.Request { 423 - if len(m.requests) == 0 { 424 - return nil 425 - } 426 - return m.requests[len(m.requests)-1] 427 - } 428 - 429 - // MockOpenLibraryResponse creates a mock OpenLibrary search response 430 func MockOpenLibraryResponse(books []MockBook) services.OpenLibrarySearchResponse { 431 docs := make([]services.OpenLibrarySearchDoc, len(books)) 432 for i, book := range books { ··· 487 Link string 488 Score string 489 Type string 490 - } 491 - 492 - // HTTPErrorMockServer creates a mock server that returns HTTP errors 493 - func HTTPErrorMockServer(statusCode int, message string) *HTTPMockServer { 494 - return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 495 - http.Error(w, message, statusCode) 496 - }) 497 - } 498 - 499 - // JSONMockServer creates a mock server that returns JSON responses 500 - func JSONMockServer(response any) *HTTPMockServer { 501 - return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 502 - w.Header().Set("Content-Type", "application/json") 503 - if err := json.NewEncoder(w).Encode(response); err != nil { 504 - http.Error(w, "Failed to encode response", http.StatusInternalServerError) 505 - } 506 - }) 507 - } 508 - 509 - // TimeoutMockServer creates a mock server that simulates timeouts 510 - func TimeoutMockServer(delay time.Duration) *HTTPMockServer { 511 - return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 512 - time.Sleep(delay) 513 - w.WriteHeader(http.StatusOK) 514 - }) 515 - } 516 - 517 - // InvalidJSONMockServer creates a mock server that returns malformed JSON 518 - func InvalidJSONMockServer() *HTTPMockServer { 519 - return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 520 - w.Header().Set("Content-Type", "application/json") 521 - w.Write([]byte(`{"invalid": json`)) 522 - }) 523 - } 524 - 525 - // EmptyResponseMockServer creates a mock server that returns empty responses 526 - func EmptyResponseMockServer() *HTTPMockServer { 527 - return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 528 - w.WriteHeader(http.StatusOK) 529 - }) 530 - } 531 - 532 - // ServiceTestHelper provides utilities for testing services with HTTP mocks 533 - type ServiceTestHelper struct { 534 - mockServers []*HTTPMockServer 535 - } 536 - 537 - // NewServiceTestHelper creates a new service test helper 538 - func NewServiceTestHelper() *ServiceTestHelper { 539 - return &ServiceTestHelper{ 540 - mockServers: make([]*HTTPMockServer, 0), 541 - } 542 - } 543 - 544 - // AddMockServer adds a mock server and returns its URL 545 - func (sth *ServiceTestHelper) AddMockServer(server *HTTPMockServer) string { 546 - sth.mockServers = append(sth.mockServers, server) 547 - return server.URL() 548 - } 549 - 550 - // Cleanup closes all mock servers 551 - func (sth *ServiceTestHelper) Cleanup() { 552 - for _, server := range sth.mockServers { 553 - server.Close() 554 - } 555 - } 556 - 557 - // AssertRequestMade verifies that a request was made to the mock server 558 - func (sth *ServiceTestHelper) AssertRequestMade(t *testing.T, server *HTTPMockServer, expectedPath string) { 559 - t.Helper() 560 - if len(server.requests) == 0 { 561 - t.Error("Expected HTTP request to be made but none were recorded") 562 - return 563 - } 564 - 565 - lastReq := server.GetLastRequest() 566 - if lastReq.URL.Path != expectedPath { 567 - t.Errorf("Expected request to path %s, got %s", expectedPath, lastReq.URL.Path) 568 - } 569 } 570 571 // MockMediaFetcher provides a test implementation of Fetchable and Searchable interfaces ··· 1013 }, 1014 } 1015 } 1016 - 1017 - // AssertTaskHasUUID verifies that a task has a non-empty UUID 1018 - func AssertTaskHasUUID(t *testing.T, task *models.Task) { 1019 - t.Helper() 1020 - if task.UUID == "" { 1021 - t.Fatal("Task should have a UUID") 1022 - } 1023 - } 1024 - 1025 - // AssertTaskDatesSet verifies that Entry and Modified timestamps are set 1026 - func AssertTaskDatesSet(t *testing.T, task *models.Task) { 1027 - t.Helper() 1028 - if task.Entry.IsZero() { 1029 - t.Error("Task Entry timestamp should be set") 1030 - } 1031 - if task.Modified.IsZero() { 1032 - t.Error("Task Modified timestamp should be set") 1033 - } 1034 - }
··· 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strconv" ··· 21 "github.com/stormlightlabs/noteleaf/internal/ui" 22 ) 23 24 + // HandlerTestHelper wraps [NoteHandler] with test-specific functionality 25 // 26 + // Uses [HandlerTestSuite] internally to avoid code duplication 27 type HandlerTestHelper struct { 28 *NoteHandler 29 suite *HandlerTestSuite 30 } 31 32 + // NewHandlerTestHelper creates a [NoteHandler] with isolated test database 33 func NewHandlerTestHelper(t *testing.T) *HandlerTestHelper { 34 suite := NewHandlerTestSuite(t) 35 ··· 123 return me 124 } 125 126 + // GetEditorFunc returns the editor function for use with [NoteHandler] 127 func (me *MockEditor) GetEditorFunc() editorFunc { 128 return func(editor, filePath string) error { 129 if me.shouldFail { ··· 189 dth.handler.db = db 190 } 191 192 // EnvironmentTestHelper provides environment manipulation utilities for testing 193 type EnvironmentTestHelper struct { 194 originalVars map[string]string 195 } ··· 252 return tempDir, nil 253 } 254 255 + // ArticleTestHelper wraps [ArticleHandler] with test-specific functionality 256 type ArticleTestHelper struct { 257 *ArticleHandler 258 suite *HandlerTestSuite 259 } 260 261 + // NewArticleTestHelper creates an [ArticleHandler] with isolated test database 262 func NewArticleTestHelper(t *testing.T) *ArticleTestHelper { 263 suite := NewHandlerTestSuite(t) 264 ··· 325 } 326 } 327 328 + // MockOpenLibraryResponse creates a mocked instance of [services.OpenLibrarySearchResponse] 329 func MockOpenLibraryResponse(books []MockBook) services.OpenLibrarySearchResponse { 330 docs := make([]services.OpenLibrarySearchDoc, len(books)) 331 for i, book := range books { ··· 386 Link string 387 Score string 388 Type string 389 } 390 391 // MockMediaFetcher provides a test implementation of Fetchable and Searchable interfaces ··· 833 }, 834 } 835 }
+52 -52
internal/models/models_test.go
··· 78 79 t.Run("Task Model", func(t *testing.T) { 80 t.Run("Status Methods", func(t *testing.T) { 81 - testCases := []struct { 82 status string 83 isCompleted bool 84 isPending bool ··· 90 {"unknown", false, false, false}, 91 } 92 93 - for _, tc := range testCases { 94 - task := &Task{Status: tc.status} 95 96 - if task.IsCompleted() != tc.isCompleted { 97 - t.Errorf("Status %s: expected IsCompleted %v, got %v", tc.status, tc.isCompleted, task.IsCompleted()) 98 } 99 - if task.IsPending() != tc.isPending { 100 - t.Errorf("Status %s: expected IsPending %v, got %v", tc.status, tc.isPending, task.IsPending()) 101 } 102 - if task.IsDeleted() != tc.isDeleted { 103 - t.Errorf("Status %s: expected IsDeleted %v, got %v", tc.status, tc.isDeleted, task.IsDeleted()) 104 } 105 } 106 }) 107 108 t.Run("New Status Tracking Methods", func(t *testing.T) { 109 - testCases := []struct { 110 status string 111 isTodo bool 112 isInProgress bool ··· 122 {"unknown", false, false, false, false, false}, 123 } 124 125 - for _, tc := range testCases { 126 - task := &Task{Status: tc.status} 127 128 - if task.IsTodo() != tc.isTodo { 129 - t.Errorf("Status %s: expected IsTodo %v, got %v", tc.status, tc.isTodo, task.IsTodo()) 130 } 131 - if task.IsInProgress() != tc.isInProgress { 132 - t.Errorf("Status %s: expected IsInProgress %v, got %v", tc.status, tc.isInProgress, task.IsInProgress()) 133 } 134 - if task.IsBlocked() != tc.isBlocked { 135 - t.Errorf("Status %s: expected IsBlocked %v, got %v", tc.status, tc.isBlocked, task.IsBlocked()) 136 } 137 - if task.IsDone() != tc.isDone { 138 - t.Errorf("Status %s: expected IsDone %v, got %v", tc.status, tc.isDone, task.IsDone()) 139 } 140 - if task.IsAbandoned() != tc.isAbandoned { 141 - t.Errorf("Status %s: expected IsAbandoned %v, got %v", tc.status, tc.isAbandoned, task.IsAbandoned()) 142 } 143 } 144 }) ··· 238 }) 239 240 t.Run("Priority Weight Calculation", func(t *testing.T) { 241 - testCases := []struct { 242 priority string 243 weight int 244 }{ ··· 258 {"invalid", 0}, 259 } 260 261 - for _, tc := range testCases { 262 - task := &Task{Priority: tc.priority} 263 weight := task.GetPriorityWeight() 264 - if weight != tc.weight { 265 - t.Errorf("Priority %s: expected weight %d, got %d", tc.priority, tc.weight, weight) 266 } 267 } 268 }) ··· 481 482 t.Run("Movie Model", func(t *testing.T) { 483 t.Run("Status Methods", func(t *testing.T) { 484 - testCases := []struct { 485 status string 486 isWatched bool 487 isQueued bool ··· 492 {"unknown", false, false}, 493 } 494 495 - for _, tc := range testCases { 496 - movie := &Movie{Status: tc.status} 497 498 - if movie.IsWatched() != tc.isWatched { 499 - t.Errorf("Status %s: expected IsWatched %v, got %v", tc.status, tc.isWatched, movie.IsWatched()) 500 } 501 - if movie.IsQueued() != tc.isQueued { 502 - t.Errorf("Status %s: expected IsQueued %v, got %v", tc.status, tc.isQueued, movie.IsQueued()) 503 } 504 } 505 }) ··· 507 508 t.Run("TV Show Model", func(t *testing.T) { 509 t.Run("Status Methods", func(t *testing.T) { 510 - testCases := []struct { 511 status string 512 isWatching bool 513 isWatched bool ··· 520 {"unknown", false, false, false}, 521 } 522 523 - for _, tc := range testCases { 524 - tvShow := &TVShow{Status: tc.status} 525 526 - if tvShow.IsWatching() != tc.isWatching { 527 - t.Errorf("Status %s: expected IsWatching %v, got %v", tc.status, tc.isWatching, tvShow.IsWatching()) 528 } 529 - if tvShow.IsWatched() != tc.isWatched { 530 - t.Errorf("Status %s: expected IsWatched %v, got %v", tc.status, tc.isWatched, tvShow.IsWatched()) 531 } 532 - if tvShow.IsQueued() != tc.isQueued { 533 - t.Errorf("Status %s: expected IsQueued %v, got %v", tc.status, tc.isQueued, tvShow.IsQueued()) 534 } 535 } 536 }) ··· 538 539 t.Run("Book Model", func(t *testing.T) { 540 t.Run("Status Methods", func(t *testing.T) { 541 - testCases := []struct { 542 status string 543 isReading bool 544 isFinished bool ··· 551 {"unknown", false, false, false}, 552 } 553 554 - for _, tc := range testCases { 555 - book := &Book{Status: tc.status} 556 557 - if book.IsReading() != tc.isReading { 558 - t.Errorf("Status %s: expected IsReading %v, got %v", tc.status, tc.isReading, book.IsReading()) 559 } 560 - if book.IsFinished() != tc.isFinished { 561 - t.Errorf("Status %s: expected IsFinished %v, got %v", tc.status, tc.isFinished, book.IsFinished()) 562 } 563 - if book.IsQueued() != tc.isQueued { 564 - t.Errorf("Status %s: expected IsQueued %v, got %v", tc.status, tc.isQueued, book.IsQueued()) 565 } 566 } 567 })
··· 78 79 t.Run("Task Model", func(t *testing.T) { 80 t.Run("Status Methods", func(t *testing.T) { 81 + tc := []struct { 82 status string 83 isCompleted bool 84 isPending bool ··· 90 {"unknown", false, false, false}, 91 } 92 93 + for _, tt := range tc { 94 + task := &Task{Status: tt.status} 95 96 + if task.IsCompleted() != tt.isCompleted { 97 + t.Errorf("Status %s: expected IsCompleted %v, got %v", tt.status, tt.isCompleted, task.IsCompleted()) 98 } 99 + if task.IsPending() != tt.isPending { 100 + t.Errorf("Status %s: expected IsPending %v, got %v", tt.status, tt.isPending, task.IsPending()) 101 } 102 + if task.IsDeleted() != tt.isDeleted { 103 + t.Errorf("Status %s: expected IsDeleted %v, got %v", tt.status, tt.isDeleted, task.IsDeleted()) 104 } 105 } 106 }) 107 108 t.Run("New Status Tracking Methods", func(t *testing.T) { 109 + tc := []struct { 110 status string 111 isTodo bool 112 isInProgress bool ··· 122 {"unknown", false, false, false, false, false}, 123 } 124 125 + for _, tt := range tc { 126 + task := &Task{Status: tt.status} 127 128 + if task.IsTodo() != tt.isTodo { 129 + t.Errorf("Status %s: expected IsTodo %v, got %v", tt.status, tt.isTodo, task.IsTodo()) 130 } 131 + if task.IsInProgress() != tt.isInProgress { 132 + t.Errorf("Status %s: expected IsInProgress %v, got %v", tt.status, tt.isInProgress, task.IsInProgress()) 133 } 134 + if task.IsBlocked() != tt.isBlocked { 135 + t.Errorf("Status %s: expected IsBlocked %v, got %v", tt.status, tt.isBlocked, task.IsBlocked()) 136 } 137 + if task.IsDone() != tt.isDone { 138 + t.Errorf("Status %s: expected IsDone %v, got %v", tt.status, tt.isDone, task.IsDone()) 139 } 140 + if task.IsAbandoned() != tt.isAbandoned { 141 + t.Errorf("Status %s: expected IsAbandoned %v, got %v", tt.status, tt.isAbandoned, task.IsAbandoned()) 142 } 143 } 144 }) ··· 238 }) 239 240 t.Run("Priority Weight Calculation", func(t *testing.T) { 241 + tc := []struct { 242 priority string 243 weight int 244 }{ ··· 258 {"invalid", 0}, 259 } 260 261 + for _, tt := range tc { 262 + task := &Task{Priority: tt.priority} 263 weight := task.GetPriorityWeight() 264 + if weight != tt.weight { 265 + t.Errorf("Priority %s: expected weight %d, got %d", tt.priority, tt.weight, weight) 266 } 267 } 268 }) ··· 481 482 t.Run("Movie Model", func(t *testing.T) { 483 t.Run("Status Methods", func(t *testing.T) { 484 + tc := []struct { 485 status string 486 isWatched bool 487 isQueued bool ··· 492 {"unknown", false, false}, 493 } 494 495 + for _, tt := range tc { 496 + movie := &Movie{Status: tt.status} 497 498 + if movie.IsWatched() != tt.isWatched { 499 + t.Errorf("Status %s: expected IsWatched %v, got %v", tt.status, tt.isWatched, movie.IsWatched()) 500 } 501 + if movie.IsQueued() != tt.isQueued { 502 + t.Errorf("Status %s: expected IsQueued %v, got %v", tt.status, tt.isQueued, movie.IsQueued()) 503 } 504 } 505 }) ··· 507 508 t.Run("TV Show Model", func(t *testing.T) { 509 t.Run("Status Methods", func(t *testing.T) { 510 + tc := []struct { 511 status string 512 isWatching bool 513 isWatched bool ··· 520 {"unknown", false, false, false}, 521 } 522 523 + for _, tt := range tc { 524 + tvShow := &TVShow{Status: tt.status} 525 526 + if tvShow.IsWatching() != tt.isWatching { 527 + t.Errorf("Status %s: expected IsWatching %v, got %v", tt.status, tt.isWatching, tvShow.IsWatching()) 528 } 529 + if tvShow.IsWatched() != tt.isWatched { 530 + t.Errorf("Status %s: expected IsWatched %v, got %v", tt.status, tt.isWatched, tvShow.IsWatched()) 531 } 532 + if tvShow.IsQueued() != tt.isQueued { 533 + t.Errorf("Status %s: expected IsQueued %v, got %v", tt.status, tt.isQueued, tvShow.IsQueued()) 534 } 535 } 536 }) ··· 538 539 t.Run("Book Model", func(t *testing.T) { 540 t.Run("Status Methods", func(t *testing.T) { 541 + tc := []struct { 542 status string 543 isReading bool 544 isFinished bool ··· 551 {"unknown", false, false, false}, 552 } 553 554 + for _, tt := range tc { 555 + book := &Book{Status: tt.status} 556 557 + if book.IsReading() != tt.isReading { 558 + t.Errorf("Status %s: expected IsReading %v, got %v", tt.status, tt.isReading, book.IsReading()) 559 } 560 + if book.IsFinished() != tt.isFinished { 561 + t.Errorf("Status %s: expected IsFinished %v, got %v", tt.status, tt.isFinished, book.IsFinished()) 562 } 563 + if book.IsQueued() != tt.isQueued { 564 + t.Errorf("Status %s: expected IsQueued %v, got %v", tt.status, tt.isQueued, book.IsQueued()) 565 } 566 } 567 })
+113 -112
internal/repo/article_repository_test.go
··· 8 9 _ "github.com/mattn/go-sqlite3" 10 "github.com/stormlightlabs/noteleaf/internal/models" 11 ) 12 13 func TestArticleRepository(t *testing.T) { ··· 20 21 article := CreateSampleArticle() 22 id, err := repo.Create(ctx, article) 23 - AssertNoError(t, err, "Failed to create article") 24 - AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 25 - AssertEqual(t, id, article.ID, "Expected article ID to be set correctly") 26 - AssertFalse(t, article.Created.IsZero(), "Expected Created timestamp to be set") 27 - AssertFalse(t, article.Modified.IsZero(), "Expected Modified timestamp to be set") 28 }) 29 30 t.Run("Get article", func(t *testing.T) { ··· 33 34 original := CreateSampleArticle() 35 id, err := repo.Create(ctx, original) 36 - AssertNoError(t, err, "Failed to create article") 37 38 retrieved, err := repo.Get(ctx, id) 39 - AssertNoError(t, err, "Failed to get article") 40 - AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") 41 - AssertEqual(t, original.URL, retrieved.URL, "URL mismatch") 42 - AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") 43 - AssertEqual(t, original.Author, retrieved.Author, "Author mismatch") 44 - AssertEqual(t, original.Date, retrieved.Date, "Date mismatch") 45 - AssertEqual(t, original.MarkdownPath, retrieved.MarkdownPath, "MarkdownPath mismatch") 46 - AssertEqual(t, original.HTMLPath, retrieved.HTMLPath, "HTMLPath mismatch") 47 }) 48 49 t.Run("Update article", func(t *testing.T) { ··· 52 53 article := CreateSampleArticle() 54 id, err := repo.Create(ctx, article) 55 - AssertNoError(t, err, "Failed to create article") 56 57 originalModified := article.Modified 58 article.Title = "Updated Title" ··· 62 article.HTMLPath = "/updated/path/article.html" 63 64 err = repo.Update(ctx, article) 65 - AssertNoError(t, err, "Failed to update article") 66 67 retrieved, err := repo.Get(ctx, id) 68 - AssertNoError(t, err, "Failed to get updated article") 69 - AssertEqual(t, "Updated Title", retrieved.Title, "Expected updated title") 70 - AssertEqual(t, "Updated Author", retrieved.Author, "Expected updated author") 71 - AssertEqual(t, "2024-01-02", retrieved.Date, "Expected updated date") 72 - AssertEqual(t, "/updated/path/article.md", retrieved.MarkdownPath, "Expected updated markdown path") 73 - AssertEqual(t, "/updated/path/article.html", retrieved.HTMLPath, "Expected updated HTML path") 74 - AssertTrue(t, retrieved.Modified.After(originalModified), "Expected Modified timestamp to be updated") 75 }) 76 77 t.Run("Delete article", func(t *testing.T) { ··· 80 81 article := CreateSampleArticle() 82 id, err := repo.Create(ctx, article) 83 - AssertNoError(t, err, "Failed to create article") 84 85 err = repo.Delete(ctx, id) 86 - AssertNoError(t, err, "Failed to delete article") 87 88 _, err = repo.Get(ctx, id) 89 - AssertError(t, err, "Expected error when getting deleted article") 90 }) 91 }) 92 ··· 99 article := CreateSampleArticle() 100 article.Title = "" 101 _, err := repo.Create(ctx, article) 102 - AssertError(t, err, "Expected error when creating article with empty title") 103 }) 104 105 t.Run("Fails with missing URL", func(t *testing.T) { 106 article := CreateSampleArticle() 107 article.URL = "" 108 _, err := repo.Create(ctx, article) 109 - AssertError(t, err, "Expected error when creating article with empty URL") 110 }) 111 112 t.Run("Fails with duplicate URL", func(t *testing.T) { 113 article1 := CreateSampleArticle() 114 _, err := repo.Create(ctx, article1) 115 - AssertNoError(t, err, "Failed to create first article") 116 117 article2 := CreateSampleArticle() 118 article2.URL = article1.URL 119 _, err = repo.Create(ctx, article2) 120 - AssertError(t, err, "Expected error when creating article with duplicate URL") 121 }) 122 123 t.Run("Fails with missing markdown path", func(t *testing.T) { 124 article := CreateSampleArticle() 125 article.MarkdownPath = "" 126 _, err := repo.Create(ctx, article) 127 - AssertError(t, err, "Expected error when creating article with empty markdown path") 128 - AssertContains(t, err.Error(), "MarkdownPath", "Expected MarkdownPath validation error") 129 }) 130 131 t.Run("Fails with missing HTML path", func(t *testing.T) { 132 article := CreateSampleArticle() 133 article.HTMLPath = "" 134 _, err := repo.Create(ctx, article) 135 - AssertError(t, err, "Expected error when creating article with empty HTML path") 136 - AssertContains(t, err.Error(), "HTMLPath", "Expected HTMLPath validation error") 137 }) 138 139 t.Run("Fails with invalid URL format", func(t *testing.T) { 140 article := CreateSampleArticle() 141 article.URL = "not-a-valid-url" 142 _, err := repo.Create(ctx, article) 143 - AssertError(t, err, "Expected error when creating article with invalid URL format") 144 - AssertContains(t, err.Error(), "URL", "Expected URL format validation error") 145 }) 146 147 t.Run("Fails with invalid date format", func(t *testing.T) { 148 article := CreateSampleArticle() 149 article.Date = "invalid-date" 150 _, err := repo.Create(ctx, article) 151 - AssertError(t, err, "Expected error when creating article with invalid date format") 152 - AssertContains(t, err.Error(), "Date", "Expected date validation error") 153 }) 154 155 t.Run("Fails with title too long", func(t *testing.T) { 156 article := CreateSampleArticle() 157 article.Title = strings.Repeat("a", 501) 158 _, err := repo.Create(ctx, article) 159 - AssertError(t, err, "Expected error when creating article with title too long") 160 - AssertContains(t, err.Error(), "Title", "Expected title length validation error") 161 }) 162 163 t.Run("Fails with author too long", func(t *testing.T) { 164 article := CreateSampleArticle() 165 article.Author = strings.Repeat("a", 201) 166 _, err := repo.Create(ctx, article) 167 - AssertError(t, err, "Expected error when creating article with author too long") 168 - AssertContains(t, err.Error(), "Author", "Expected author length validation error") 169 }) 170 171 t.Run("Validates timestamps", func(t *testing.T) { ··· 174 article.Modified = now 175 article.Created = now.Add(time.Hour) 176 err := repo.Validate(article) 177 - AssertError(t, err, "Expected error when created is after modified") 178 - AssertContains(t, err.Error(), "Created", "Expected timestamp validation error") 179 }) 180 181 t.Run("Succeeds when created equals modified", func(t *testing.T) { ··· 184 article.Created = now 185 article.Modified = now 186 err := repo.Validate(article) 187 - AssertNoError(t, err, "Expected no error when created equals modified") 188 }) 189 190 t.Run("Succeeds when created is before modified", func(t *testing.T) { ··· 193 article.Created = now 194 article.Modified = now.Add(time.Hour) 195 err := repo.Validate(article) 196 - AssertNoError(t, err, "Expected no error when created is before modified") 197 }) 198 199 t.Run("Succeeds with valid optional fields", func(t *testing.T) { ··· 201 article.Date = "2024-01-01" 202 article.Author = "Test Author" 203 err := repo.Validate(article) 204 - AssertNoError(t, err, "Expected no error with valid optional fields") 205 }) 206 207 t.Run("Succeeds with empty optional fields", func(t *testing.T) { ··· 209 article.Date = "" 210 article.Author = "" 211 err := repo.Validate(article) 212 - AssertNoError(t, err, "Expected no error with empty optional fields") 213 }) 214 }) 215 ··· 221 t.Run("Successfully retrieves article by URL", func(t *testing.T) { 222 original := CreateSampleArticle() 223 _, err := repo.Create(ctx, original) 224 - AssertNoError(t, err, "Failed to create article") 225 226 retrieved, err := repo.GetByURL(ctx, original.URL) 227 - AssertNoError(t, err, "Failed to get article by URL") 228 - AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") 229 - AssertEqual(t, original.URL, retrieved.URL, "URL mismatch") 230 - AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") 231 }) 232 233 t.Run("Fails when URL not found", func(t *testing.T) { 234 nonexistent := "https://example.com/nonexistent" 235 _, err := repo.GetByURL(ctx, nonexistent) 236 - AssertError(t, err, "Expected error when getting article by non-existent URL") 237 - AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") 238 }) 239 }) 240 ··· 272 273 for _, article := range articles { 274 _, err := repo.Create(ctx, article) 275 - AssertNoError(t, err, "Failed to create test article") 276 } 277 278 t.Run("List all articles", func(t *testing.T) { 279 results, err := repo.List(ctx, nil) 280 - AssertNoError(t, err, "Failed to list all articles") 281 - AssertEqual(t, 3, len(results), "Expected 3 articles") 282 }) 283 284 t.Run("Filter by title", func(t *testing.T) { 285 opts := &ArticleListOptions{Title: "Important"} 286 results, err := repo.List(ctx, opts) 287 - AssertNoError(t, err, "Failed to list articles by title") 288 - AssertEqual(t, 1, len(results), "Expected 1 article matching title") 289 - AssertEqual(t, "Important Article", results[0].Title, "Wrong article returned") 290 }) 291 292 t.Run("Filter by author", func(t *testing.T) { 293 opts := &ArticleListOptions{Author: "John Doe"} 294 results, err := repo.List(ctx, opts) 295 - AssertNoError(t, err, "Failed to list articles by author") 296 - AssertEqual(t, 2, len(results), "Expected 2 articles by John Doe") 297 }) 298 299 t.Run("Filter by URL", func(t *testing.T) { 300 opts := &ArticleListOptions{URL: "different.com"} 301 results, err := repo.List(ctx, opts) 302 - AssertNoError(t, err, "Failed to list articles by URL") 303 - AssertEqual(t, 1, len(results), "Expected 1 article from different.com") 304 }) 305 306 t.Run("Filter by date range", func(t *testing.T) { 307 opts := &ArticleListOptions{DateFrom: "2024-01-02", DateTo: "2024-01-03"} 308 results, err := repo.List(ctx, opts) 309 - AssertNoError(t, err, "Failed to list articles by date range") 310 - AssertEqual(t, 2, len(results), "Expected 2 articles in date range") 311 }) 312 313 t.Run("With limit", func(t *testing.T) { 314 opts := &ArticleListOptions{Limit: 2} 315 results, err := repo.List(ctx, opts) 316 - AssertNoError(t, err, "Failed to list articles with limit") 317 - AssertEqual(t, 2, len(results), "Expected 2 articles due to limit") 318 }) 319 320 t.Run("With limit and offset", func(t *testing.T) { 321 opts := &ArticleListOptions{Limit: 2, Offset: 1} 322 results, err := repo.List(ctx, opts) 323 - AssertNoError(t, err, "Failed to list articles with limit and offset") 324 - AssertEqual(t, 2, len(results), "Expected 2 articles due to limit") 325 }) 326 327 t.Run("Multiple filters", func(t *testing.T) { 328 opts := &ArticleListOptions{Author: "John Doe", DateFrom: "2024-01-02"} 329 results, err := repo.List(ctx, opts) 330 - AssertNoError(t, err, "Failed to list articles with multiple filters") 331 - AssertEqual(t, 1, len(results), "Expected 1 article matching all filters") 332 - AssertEqual(t, "Important Article", results[0].Title, "Wrong article returned") 333 }) 334 335 t.Run("No results", func(t *testing.T) { 336 opts := &ArticleListOptions{Title: "Nonexistent"} 337 results, err := repo.List(ctx, opts) 338 - AssertNoError(t, err, "Failed to list articles") 339 - AssertEqual(t, 0, len(results), "Expected no articles") 340 }) 341 }) 342 ··· 359 360 for _, article := range articles { 361 _, err := repo.Create(ctx, article) 362 - AssertNoError(t, err, "Failed to create test article") 363 } 364 365 t.Run("Count all articles", func(t *testing.T) { 366 count, err := repo.Count(ctx, nil) 367 - AssertNoError(t, err, "Failed to count articles") 368 - AssertEqual(t, int64(2), count, "Expected 2 articles") 369 }) 370 371 t.Run("Count with filter", func(t *testing.T) { 372 opts := &ArticleListOptions{Author: "Test Author"} 373 count, err := repo.Count(ctx, opts) 374 - AssertNoError(t, err, "Failed to count articles with filter") 375 - AssertEqual(t, int64(1), count, "Expected 1 article by Test Author") 376 }) 377 378 t.Run("Count with no results", func(t *testing.T) { 379 opts := &ArticleListOptions{Title: "Nonexistent"} 380 count, err := repo.Count(ctx, opts) 381 - AssertNoError(t, err, "Failed to count articles") 382 - AssertEqual(t, int64(0), count, "Expected 0 articles") 383 }) 384 }) 385 ··· 390 391 article := CreateSampleArticle() 392 id, err := repo.Create(ctx, article) 393 - AssertNoError(t, err, "Failed to create article") 394 395 t.Run("Create with cancelled context", func(t *testing.T) { 396 newArticle := CreateSampleArticle() ··· 437 438 t.Run("Get non-existent article", func(t *testing.T) { 439 _, err := repo.Get(ctx, 99999) 440 - AssertError(t, err, "Expected error for non-existent article") 441 - AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") 442 }) 443 444 t.Run("Update non-existent article", func(t *testing.T) { 445 article := CreateSampleArticle() 446 article.ID = 99999 447 err := repo.Update(ctx, article) 448 - AssertError(t, err, "Expected error when updating non-existent article") 449 - AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") 450 }) 451 452 t.Run("Delete non-existent article", func(t *testing.T) { 453 err := repo.Delete(ctx, 99999) 454 - AssertError(t, err, "Expected error when deleting non-existent article") 455 - AssertContains(t, err.Error(), "not found", "Expected 'not found' in error message") 456 }) 457 458 t.Run("Update validation - remove required title", func(t *testing.T) { ··· 461 462 article := CreateSampleArticle() 463 _, err := repo.Create(ctx, article) 464 - AssertNoError(t, err, "Failed to create article") 465 466 article.Title = "" 467 err = repo.Update(ctx, article) 468 - AssertError(t, err, "Expected error when updating article with empty title") 469 }) 470 471 t.Run("Update validation - invalid URL format", func(t *testing.T) { ··· 474 475 article := CreateSampleArticle() 476 _, err := repo.Create(ctx, article) 477 - AssertNoError(t, err, "Failed to create article") 478 479 article.URL = "not-a-valid-url" 480 err = repo.Update(ctx, article) 481 - AssertError(t, err, "Expected error when updating article with invalid URL format") 482 - AssertContains(t, err.Error(), "URL", "Expected URL format validation error") 483 }) 484 485 t.Run("Update validation - invalid date format", func(t *testing.T) { ··· 488 489 article := CreateSampleArticle() 490 _, err := repo.Create(ctx, article) 491 - AssertNoError(t, err, "Failed to create article") 492 493 article.Date = "invalid-date" 494 err = repo.Update(ctx, article) 495 - AssertError(t, err, "Expected error when updating article with invalid date format") 496 - AssertContains(t, err.Error(), "Date", "Expected date validation error") 497 }) 498 499 t.Run("Update validation - title too long", func(t *testing.T) { ··· 502 503 article := CreateSampleArticle() 504 _, err := repo.Create(ctx, article) 505 - AssertNoError(t, err, "Failed to create article") 506 507 article.Title = strings.Repeat("a", 501) 508 err = repo.Update(ctx, article) 509 - AssertError(t, err, "Expected error when updating article with title too long") 510 - AssertContains(t, err.Error(), "Title", "Expected title length validation error") 511 }) 512 513 t.Run("Update validation - author too long", func(t *testing.T) { ··· 516 517 article := CreateSampleArticle() 518 _, err := repo.Create(ctx, article) 519 - AssertNoError(t, err, "Failed to create article") 520 521 article.Author = strings.Repeat("a", 201) 522 err = repo.Update(ctx, article) 523 - AssertError(t, err, "Expected error when updating article with author too long") 524 - AssertContains(t, err.Error(), "Author", "Expected author length validation error") 525 }) 526 527 t.Run("Update validation - remove markdown path", func(t *testing.T) { ··· 530 531 article := CreateSampleArticle() 532 _, err := repo.Create(ctx, article) 533 - AssertNoError(t, err, "Failed to create article") 534 535 article.MarkdownPath = "" 536 err = repo.Update(ctx, article) 537 - AssertError(t, err, "Expected error when updating article with empty markdown path") 538 - AssertContains(t, err.Error(), "MarkdownPath", "Expected MarkdownPath validation error") 539 }) 540 541 t.Run("Update validation - remove HTML path", func(t *testing.T) { ··· 544 545 article := CreateSampleArticle() 546 _, err := repo.Create(ctx, article) 547 - AssertNoError(t, err, "Failed to create article") 548 549 article.HTMLPath = "" 550 err = repo.Update(ctx, article) 551 - AssertError(t, err, "Expected error when updating article with empty HTML path") 552 - AssertContains(t, err.Error(), "HTMLPath", "Expected HTMLPath validation error") 553 }) 554 555 t.Run("List with no results", func(t *testing.T) { 556 opts := &ArticleListOptions{Author: "NonExistentAuthor"} 557 articles, err := repo.List(ctx, opts) 558 - AssertNoError(t, err, "Should not error when no articles found") 559 - AssertEqual(t, 0, len(articles), "Expected empty result set") 560 }) 561 }) 562 }
··· 8 9 _ "github.com/mattn/go-sqlite3" 10 "github.com/stormlightlabs/noteleaf/internal/models" 11 + "github.com/stormlightlabs/noteleaf/internal/shared" 12 ) 13 14 func TestArticleRepository(t *testing.T) { ··· 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) { ··· 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) { ··· 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" ··· 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) { ··· 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 ··· 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) { ··· 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) { ··· 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) { ··· 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) { ··· 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) { ··· 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 ··· 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 ··· 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 ··· 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 ··· 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() ··· 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) { ··· 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) { ··· 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) { ··· 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) { ··· 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) { ··· 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) { ··· 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) { ··· 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 }
+54 -53
internal/repo/base_media_repository_test.go
··· 6 7 _ "github.com/mattn/go-sqlite3" 8 "github.com/stormlightlabs/noteleaf/internal/models" 9 ) 10 11 func TestBaseMediaRepository(t *testing.T) { ··· 23 } 24 25 id, err := repo.Create(ctx, book) 26 - AssertNoError(t, err, "Failed to create book") 27 - AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 28 29 retrieved, err := repo.Get(ctx, id) 30 - AssertNoError(t, err, "Failed to get book") 31 - AssertEqual(t, book.Title, retrieved.Title, "Title mismatch") 32 - AssertEqual(t, book.Author, retrieved.Author, "Author mismatch") 33 - AssertEqual(t, book.Status, retrieved.Status, "Status mismatch") 34 }) 35 36 t.Run("Update", func(t *testing.T) { ··· 44 } 45 46 id, err := repo.Create(ctx, book) 47 - AssertNoError(t, err, "Failed to create book") 48 49 book.Title = "Updated Title" 50 book.Author = "Updated Author" 51 book.Status = "reading" 52 53 err = repo.Update(ctx, book) 54 - AssertNoError(t, err, "Failed to update book") 55 56 retrieved, err := repo.Get(ctx, id) 57 - AssertNoError(t, err, "Failed to get updated book") 58 - AssertEqual(t, "Updated Title", retrieved.Title, "Title not updated") 59 - AssertEqual(t, "Updated Author", retrieved.Author, "Author not updated") 60 - AssertEqual(t, "reading", retrieved.Status, "Status not updated") 61 }) 62 63 t.Run("Delete", func(t *testing.T) { ··· 70 } 71 72 id, err := repo.Create(ctx, book) 73 - AssertNoError(t, err, "Failed to create book") 74 75 err = repo.Delete(ctx, id) 76 - AssertNoError(t, err, "Failed to delete book") 77 78 _, err = repo.Get(ctx, id) 79 - AssertError(t, err, "Expected error when getting deleted book") 80 }) 81 82 t.Run("Get non-existent", func(t *testing.T) { ··· 84 repo := NewBookRepository(db) 85 86 _, err := repo.Get(ctx, 9999) 87 - AssertError(t, err, "Expected error for non-existent book") 88 - AssertContains(t, err.Error(), "not found", "Error should mention 'not found'") 89 }) 90 91 t.Run("ListQuery with multiple books", func(t *testing.T) { ··· 100 101 for _, book := range books { 102 _, err := repo.Create(ctx, book) 103 - AssertNoError(t, err, "Failed to create book") 104 } 105 106 allBooks, err := repo.List(ctx, BookListOptions{}) 107 - AssertNoError(t, err, "Failed to list books") 108 if len(allBooks) != 3 { 109 t.Errorf("Expected 3 books, got %d", len(allBooks)) 110 } ··· 120 Status: "queued", 121 } 122 _, err := repo.Create(ctx, book) 123 - AssertNoError(t, err, "Failed to create book") 124 } 125 126 count, err := repo.Count(ctx, BookListOptions{}) 127 - AssertNoError(t, err, "Failed to count books") 128 if count != 5 { 129 t.Errorf("Expected count of 5, got %d", count) 130 } ··· 143 } 144 145 id, err := repo.Create(ctx, movie) 146 - AssertNoError(t, err, "Failed to create movie") 147 - AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 148 149 retrieved, err := repo.Get(ctx, id) 150 - AssertNoError(t, err, "Failed to get movie") 151 - AssertEqual(t, movie.Title, retrieved.Title, "Title mismatch") 152 - AssertEqual(t, movie.Year, retrieved.Year, "Year mismatch") 153 - AssertEqual(t, movie.Status, retrieved.Status, "Status mismatch") 154 }) 155 156 t.Run("Update", func(t *testing.T) { ··· 164 } 165 166 id, err := repo.Create(ctx, movie) 167 - AssertNoError(t, err, "Failed to create movie") 168 169 movie.Title = "Updated Movie" 170 movie.Year = 2023 171 movie.Status = "watched" 172 173 err = repo.Update(ctx, movie) 174 - AssertNoError(t, err, "Failed to update movie") 175 176 retrieved, err := repo.Get(ctx, id) 177 - AssertNoError(t, err, "Failed to get updated movie") 178 - AssertEqual(t, "Updated Movie", retrieved.Title, "Title not updated") 179 - AssertEqual(t, 2023, retrieved.Year, "Year not updated") 180 - AssertEqual(t, "watched", retrieved.Status, "Status not updated") 181 }) 182 183 t.Run("Delete", func(t *testing.T) { ··· 190 } 191 192 id, err := repo.Create(ctx, movie) 193 - AssertNoError(t, err, "Failed to create movie") 194 195 err = repo.Delete(ctx, id) 196 - AssertNoError(t, err, "Failed to delete movie") 197 198 _, err = repo.Get(ctx, id) 199 - AssertError(t, err, "Expected error when getting deleted movie") 200 }) 201 }) 202 ··· 213 } 214 215 id, err := repo.Create(ctx, show) 216 - AssertNoError(t, err, "Failed to create TV show") 217 - AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 218 219 retrieved, err := repo.Get(ctx, id) 220 - AssertNoError(t, err, "Failed to get TV show") 221 - AssertEqual(t, show.Title, retrieved.Title, "Title mismatch") 222 - AssertEqual(t, show.Season, retrieved.Season, "Season mismatch") 223 - AssertEqual(t, show.Episode, retrieved.Episode, "Episode mismatch") 224 - AssertEqual(t, show.Status, retrieved.Status, "Status mismatch") 225 }) 226 227 t.Run("Update", func(t *testing.T) { ··· 236 } 237 238 id, err := repo.Create(ctx, show) 239 - AssertNoError(t, err, "Failed to create TV show") 240 241 show.Title = "Updated Show" 242 show.Season = 2 ··· 244 show.Status = "watching" 245 246 err = repo.Update(ctx, show) 247 - AssertNoError(t, err, "Failed to update TV show") 248 249 retrieved, err := repo.Get(ctx, id) 250 - AssertNoError(t, err, "Failed to get updated TV show") 251 - AssertEqual(t, "Updated Show", retrieved.Title, "Title not updated") 252 - AssertEqual(t, 2, retrieved.Season, "Season not updated") 253 - AssertEqual(t, 5, retrieved.Episode, "Episode not updated") 254 - AssertEqual(t, "watching", retrieved.Status, "Status not updated") 255 }) 256 257 t.Run("Delete", func(t *testing.T) { ··· 264 } 265 266 id, err := repo.Create(ctx, show) 267 - AssertNoError(t, err, "Failed to create TV show") 268 269 err = repo.Delete(ctx, id) 270 - AssertNoError(t, err, "Failed to delete TV show") 271 272 _, err = repo.Get(ctx, id) 273 - AssertError(t, err, "Expected error when getting deleted TV show") 274 }) 275 }) 276
··· 6 7 _ "github.com/mattn/go-sqlite3" 8 "github.com/stormlightlabs/noteleaf/internal/models" 9 + "github.com/stormlightlabs/noteleaf/internal/shared" 10 ) 11 12 func TestBaseMediaRepository(t *testing.T) { ··· 24 } 25 26 id, err := repo.Create(ctx, book) 27 + shared.AssertNoError(t, err, "Failed to create book") 28 + shared.AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 29 30 retrieved, err := repo.Get(ctx, id) 31 + shared.AssertNoError(t, err, "Failed to get book") 32 + shared.AssertEqual(t, book.Title, retrieved.Title, "Title mismatch") 33 + shared.AssertEqual(t, book.Author, retrieved.Author, "Author mismatch") 34 + shared.AssertEqual(t, book.Status, retrieved.Status, "Status mismatch") 35 }) 36 37 t.Run("Update", func(t *testing.T) { ··· 45 } 46 47 id, err := repo.Create(ctx, book) 48 + shared.AssertNoError(t, err, "Failed to create book") 49 50 book.Title = "Updated Title" 51 book.Author = "Updated Author" 52 book.Status = "reading" 53 54 err = repo.Update(ctx, book) 55 + shared.AssertNoError(t, err, "Failed to update book") 56 57 retrieved, err := repo.Get(ctx, id) 58 + shared.AssertNoError(t, err, "Failed to get updated book") 59 + shared.AssertEqual(t, "Updated Title", retrieved.Title, "Title not updated") 60 + shared.AssertEqual(t, "Updated Author", retrieved.Author, "Author not updated") 61 + shared.AssertEqual(t, "reading", retrieved.Status, "Status not updated") 62 }) 63 64 t.Run("Delete", func(t *testing.T) { ··· 71 } 72 73 id, err := repo.Create(ctx, book) 74 + shared.AssertNoError(t, err, "Failed to create book") 75 76 err = repo.Delete(ctx, id) 77 + shared.AssertNoError(t, err, "Failed to delete book") 78 79 _, err = repo.Get(ctx, id) 80 + shared.AssertError(t, err, "Expected error when getting deleted book") 81 }) 82 83 t.Run("Get non-existent", func(t *testing.T) { ··· 85 repo := NewBookRepository(db) 86 87 _, err := repo.Get(ctx, 9999) 88 + shared.AssertError(t, err, "Expected error for non-existent book") 89 + shared.AssertContains(t, err.Error(), "not found", "Error should mention 'not found'") 90 }) 91 92 t.Run("ListQuery with multiple books", func(t *testing.T) { ··· 101 102 for _, book := range books { 103 _, err := repo.Create(ctx, book) 104 + shared.AssertNoError(t, err, "Failed to create book") 105 } 106 107 allBooks, err := repo.List(ctx, BookListOptions{}) 108 + shared.AssertNoError(t, err, "Failed to list books") 109 if len(allBooks) != 3 { 110 t.Errorf("Expected 3 books, got %d", len(allBooks)) 111 } ··· 121 Status: "queued", 122 } 123 _, err := repo.Create(ctx, book) 124 + shared.AssertNoError(t, err, "Failed to create book") 125 } 126 127 count, err := repo.Count(ctx, BookListOptions{}) 128 + shared.AssertNoError(t, err, "Failed to count books") 129 if count != 5 { 130 t.Errorf("Expected count of 5, got %d", count) 131 } ··· 144 } 145 146 id, err := repo.Create(ctx, movie) 147 + shared.AssertNoError(t, err, "Failed to create movie") 148 + shared.AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 149 150 retrieved, err := repo.Get(ctx, id) 151 + shared.AssertNoError(t, err, "Failed to get movie") 152 + shared.AssertEqual(t, movie.Title, retrieved.Title, "Title mismatch") 153 + shared.AssertEqual(t, movie.Year, retrieved.Year, "Year mismatch") 154 + shared.AssertEqual(t, movie.Status, retrieved.Status, "Status mismatch") 155 }) 156 157 t.Run("Update", func(t *testing.T) { ··· 165 } 166 167 id, err := repo.Create(ctx, movie) 168 + shared.AssertNoError(t, err, "Failed to create movie") 169 170 movie.Title = "Updated Movie" 171 movie.Year = 2023 172 movie.Status = "watched" 173 174 err = repo.Update(ctx, movie) 175 + shared.AssertNoError(t, err, "Failed to update movie") 176 177 retrieved, err := repo.Get(ctx, id) 178 + shared.AssertNoError(t, err, "Failed to get updated movie") 179 + shared.AssertEqual(t, "Updated Movie", retrieved.Title, "Title not updated") 180 + shared.AssertEqual(t, 2023, retrieved.Year, "Year not updated") 181 + shared.AssertEqual(t, "watched", retrieved.Status, "Status not updated") 182 }) 183 184 t.Run("Delete", func(t *testing.T) { ··· 191 } 192 193 id, err := repo.Create(ctx, movie) 194 + shared.AssertNoError(t, err, "Failed to create movie") 195 196 err = repo.Delete(ctx, id) 197 + shared.AssertNoError(t, err, "Failed to delete movie") 198 199 _, err = repo.Get(ctx, id) 200 + shared.AssertError(t, err, "Expected error when getting deleted movie") 201 }) 202 }) 203 ··· 214 } 215 216 id, err := repo.Create(ctx, show) 217 + shared.AssertNoError(t, err, "Failed to create TV show") 218 + shared.AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 219 220 retrieved, err := repo.Get(ctx, id) 221 + shared.AssertNoError(t, err, "Failed to get TV show") 222 + shared.AssertEqual(t, show.Title, retrieved.Title, "Title mismatch") 223 + shared.AssertEqual(t, show.Season, retrieved.Season, "Season mismatch") 224 + shared.AssertEqual(t, show.Episode, retrieved.Episode, "Episode mismatch") 225 + shared.AssertEqual(t, show.Status, retrieved.Status, "Status mismatch") 226 }) 227 228 t.Run("Update", func(t *testing.T) { ··· 237 } 238 239 id, err := repo.Create(ctx, show) 240 + shared.AssertNoError(t, err, "Failed to create TV show") 241 242 show.Title = "Updated Show" 243 show.Season = 2 ··· 245 show.Status = "watching" 246 247 err = repo.Update(ctx, show) 248 + shared.AssertNoError(t, err, "Failed to update TV show") 249 250 retrieved, err := repo.Get(ctx, id) 251 + shared.AssertNoError(t, err, "Failed to get updated TV show") 252 + shared.AssertEqual(t, "Updated Show", retrieved.Title, "Title not updated") 253 + shared.AssertEqual(t, 2, retrieved.Season, "Season not updated") 254 + shared.AssertEqual(t, 5, retrieved.Episode, "Episode not updated") 255 + shared.AssertEqual(t, "watching", retrieved.Status, "Status not updated") 256 }) 257 258 t.Run("Delete", func(t *testing.T) { ··· 265 } 266 267 id, err := repo.Create(ctx, show) 268 + shared.AssertNoError(t, err, "Failed to create TV show") 269 270 err = repo.Delete(ctx, id) 271 + shared.AssertNoError(t, err, "Failed to delete TV show") 272 273 _, err = repo.Get(ctx, id) 274 + shared.AssertError(t, err, "Expected error when getting deleted TV show") 275 }) 276 }) 277
+97 -96
internal/repo/book_repository_test.go
··· 7 8 _ "github.com/mattn/go-sqlite3" 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 ) 11 12 func TestBookRepository(t *testing.T) { ··· 19 book := CreateSampleBook() 20 21 id, err := repo.Create(ctx, book) 22 - AssertNoError(t, err, "Failed to create book") 23 - AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 24 - AssertEqual(t, id, book.ID, "Expected book ID to be set correctly") 25 - AssertFalse(t, book.Added.IsZero(), "Expected Added timestamp to be set") 26 }) 27 28 t.Run("Get Book", func(t *testing.T) { 29 original := CreateSampleBook() 30 id, err := repo.Create(ctx, original) 31 - AssertNoError(t, err, "Failed to create book") 32 33 retrieved, err := repo.Get(ctx, id) 34 - AssertNoError(t, err, "Failed to get book") 35 36 - AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") 37 - AssertEqual(t, original.Author, retrieved.Author, "Author mismatch") 38 - AssertEqual(t, original.Status, retrieved.Status, "Status mismatch") 39 - AssertEqual(t, original.Progress, retrieved.Progress, "Progress mismatch") 40 - AssertEqual(t, original.Pages, retrieved.Pages, "Pages mismatch") 41 - AssertEqual(t, original.Rating, retrieved.Rating, "Rating mismatch") 42 - AssertEqual(t, original.Notes, retrieved.Notes, "Notes mismatch") 43 }) 44 45 t.Run("Update Book", func(t *testing.T) { 46 book := CreateSampleBook() 47 id, err := repo.Create(ctx, book) 48 - AssertNoError(t, err, "Failed to create book") 49 50 book.Title = "Updated Book" 51 book.Status = "reading" ··· 55 book.Started = &now 56 57 err = repo.Update(ctx, book) 58 - AssertNoError(t, err, "Failed to update book") 59 60 updated, err := repo.Get(ctx, id) 61 - AssertNoError(t, err, "Failed to get updated book") 62 63 - AssertEqual(t, "Updated Book", updated.Title, "Expected updated title") 64 - AssertEqual(t, "reading", updated.Status, "Expected reading status") 65 - AssertEqual(t, 50, updated.Progress, "Expected progress 50") 66 - AssertEqual(t, 5.0, updated.Rating, "Expected rating 5.0") 67 - AssertTrue(t, updated.Started != nil, "Expected started time to be set") 68 }) 69 70 t.Run("Delete Book", func(t *testing.T) { 71 book := CreateSampleBook() 72 id, err := repo.Create(ctx, book) 73 - AssertNoError(t, err, "Failed to create book") 74 75 err = repo.Delete(ctx, id) 76 - AssertNoError(t, err, "Failed to delete book") 77 78 _, err = repo.Get(ctx, id) 79 - AssertError(t, err, "Expected error when getting deleted book") 80 }) 81 }) 82 ··· 94 95 for _, book := range books { 96 _, err := repo.Create(ctx, book) 97 - AssertNoError(t, err, "Failed to create book") 98 } 99 100 t.Run("List All Books", func(t *testing.T) { 101 results, err := repo.List(ctx, BookListOptions{}) 102 - AssertNoError(t, err, "Failed to list books") 103 - AssertEqual(t, 4, len(results), "Expected 4 books") 104 }) 105 106 t.Run("List Books with Status Filter", func(t *testing.T) { 107 results, err := repo.List(ctx, BookListOptions{Status: "queued"}) 108 - AssertNoError(t, err, "Failed to list books") 109 - AssertEqual(t, 2, len(results), "Expected 2 queued books") 110 111 for _, book := range results { 112 - AssertEqual(t, "queued", book.Status, "Expected queued status") 113 } 114 }) 115 116 t.Run("List Books by Author", func(t *testing.T) { 117 results, err := repo.List(ctx, BookListOptions{Author: "Author A"}) 118 - AssertNoError(t, err, "Failed to list books") 119 - AssertEqual(t, 2, len(results), "Expected 2 books by Author A") 120 121 for _, book := range results { 122 - AssertEqual(t, "Author A", book.Author, "Expected author 'Author A'") 123 } 124 }) 125 126 t.Run("List Books with Progress Filter", func(t *testing.T) { 127 results, err := repo.List(ctx, BookListOptions{MinProgress: 50}) 128 - AssertNoError(t, err, "Failed to list books") 129 - AssertEqual(t, 2, len(results), "Expected 2 books with progress >= 50") 130 131 for _, book := range results { 132 - AssertTrue(t, book.Progress >= 50, "Expected progress >= 50") 133 } 134 }) 135 136 t.Run("List Books with Rating Filter", func(t *testing.T) { 137 results, err := repo.List(ctx, BookListOptions{MinRating: 4.5}) 138 - AssertNoError(t, err, "Failed to list books") 139 - AssertEqual(t, 2, len(results), "Expected 2 books with rating >= 4.5") 140 141 for _, book := range results { 142 - AssertTrue(t, book.Rating >= 4.5, "Expected rating >= 4.5") 143 } 144 }) 145 146 t.Run("List Books with Search", func(t *testing.T) { 147 results, err := repo.List(ctx, BookListOptions{Search: "Book 1"}) 148 - AssertNoError(t, err, "Failed to list books") 149 - AssertEqual(t, 1, len(results), "Expected 1 book matching search") 150 151 if len(results) > 0 { 152 - AssertEqual(t, "Book 1", results[0].Title, "Expected 'Book 1'") 153 } 154 }) 155 156 t.Run("List Books with Limit", func(t *testing.T) { 157 results, err := repo.List(ctx, BookListOptions{Limit: 2}) 158 - AssertNoError(t, err, "Failed to list books") 159 - AssertEqual(t, 2, len(results), "Expected 2 books due to limit") 160 }) 161 }) 162 ··· 173 var book1ID int64 174 for _, book := range []*models.Book{book1, book2, book3, book4} { 175 id, err := repo.Create(ctx, book) 176 - AssertNoError(t, err, "Failed to create book") 177 if book == book1 { 178 book1ID = id 179 } ··· 181 182 t.Run("GetQueued", func(t *testing.T) { 183 results, err := repo.GetQueued(ctx) 184 - AssertNoError(t, err, "Failed to get queued books") 185 - AssertEqual(t, 2, len(results), "Expected 2 queued books") 186 187 for _, book := range results { 188 - AssertEqual(t, "queued", book.Status, "Expected queued status") 189 } 190 }) 191 192 t.Run("GetReading", func(t *testing.T) { 193 results, err := repo.GetReading(ctx) 194 - AssertNoError(t, err, "Failed to get reading books") 195 - AssertEqual(t, 1, len(results), "Expected 1 reading book") 196 197 if len(results) > 0 { 198 - AssertEqual(t, "reading", results[0].Status, "Expected reading status") 199 } 200 }) 201 202 t.Run("GetFinished", func(t *testing.T) { 203 results, err := repo.GetFinished(ctx) 204 - AssertNoError(t, err, "Failed to get finished books") 205 - AssertEqual(t, 1, len(results), "Expected 1 finished book") 206 207 if len(results) > 0 { 208 - AssertEqual(t, "finished", results[0].Status, "Expected finished status") 209 } 210 }) 211 212 t.Run("GetByAuthor", func(t *testing.T) { 213 results, err := repo.GetByAuthor(ctx, "Author A") 214 - AssertNoError(t, err, "Failed to get books by author") 215 - AssertEqual(t, 2, len(results), "Expected 2 books by Author A") 216 217 for _, book := range results { 218 - AssertEqual(t, "Author A", book.Author, "Expected author 'Author A'") 219 } 220 }) 221 222 t.Run("StartReading", func(t *testing.T) { 223 err := repo.StartReading(ctx, book1ID) 224 - AssertNoError(t, err, "Failed to start reading book") 225 226 updated, err := repo.Get(ctx, book1ID) 227 - AssertNoError(t, err, "Failed to get updated book") 228 229 - AssertEqual(t, "reading", updated.Status, "Expected status to be reading") 230 - AssertTrue(t, updated.Started != nil, "Expected started timestamp to be set") 231 }) 232 233 t.Run("FinishReading", func(t *testing.T) { 234 newBook := &models.Book{Title: "New Book", Status: "reading", Progress: 80} 235 id, err := repo.Create(ctx, newBook) 236 - AssertNoError(t, err, "Failed to create new book") 237 238 err = repo.FinishReading(ctx, id) 239 - AssertNoError(t, err, "Failed to finish reading book") 240 241 updated, err := repo.Get(ctx, id) 242 - AssertNoError(t, err, "Failed to get updated book") 243 244 - AssertEqual(t, "finished", updated.Status, "Expected status to be finished") 245 - AssertEqual(t, 100, updated.Progress, "Expected progress to be 100") 246 - AssertTrue(t, updated.Finished != nil, "Expected finished timestamp to be set") 247 }) 248 249 t.Run("UpdateProgress", func(t *testing.T) { 250 newBook := &models.Book{Title: "Progress Book", Status: "queued", Progress: 0} 251 id, err := repo.Create(ctx, newBook) 252 - AssertNoError(t, err, "Failed to create new book") 253 254 err = repo.UpdateProgress(ctx, id, 25) 255 - AssertNoError(t, err, "Failed to update progress") 256 257 updated, err := repo.Get(ctx, id) 258 - AssertNoError(t, err, "Failed to get updated book") 259 260 - AssertEqual(t, "reading", updated.Status, "Expected status to be reading when progress > 0") 261 - AssertEqual(t, 25, updated.Progress, "Expected progress 25") 262 - AssertTrue(t, updated.Started != nil, "Expected started timestamp to be set when progress > 0") 263 264 err = repo.UpdateProgress(ctx, id, 100) 265 - AssertNoError(t, err, "Failed to update progress to 100") 266 267 updated, err = repo.Get(ctx, id) 268 - AssertNoError(t, err, "Failed to get updated book") 269 270 - AssertEqual(t, "finished", updated.Status, "Expected status to be finished when progress = 100") 271 - AssertEqual(t, 100, updated.Progress, "Expected progress 100") 272 - AssertTrue(t, updated.Finished != nil, "Expected finished timestamp to be set when progress = 100") 273 }) 274 }) 275 ··· 287 288 for _, book := range books { 289 _, err := repo.Create(ctx, book) 290 - AssertNoError(t, err, "Failed to create book") 291 } 292 293 t.Run("Count all books", func(t *testing.T) { 294 count, err := repo.Count(ctx, BookListOptions{}) 295 - AssertNoError(t, err, "Failed to count books") 296 - AssertEqual(t, int64(4), count, "Expected 4 books") 297 }) 298 299 t.Run("Count queued books", func(t *testing.T) { 300 count, err := repo.Count(ctx, BookListOptions{Status: "queued"}) 301 - AssertNoError(t, err, "Failed to count queued books") 302 - AssertEqual(t, int64(2), count, "Expected 2 queued books") 303 }) 304 305 t.Run("Count books by progress", func(t *testing.T) { 306 count, err := repo.Count(ctx, BookListOptions{MinProgress: 50}) 307 - AssertNoError(t, err, "Failed to count books with progress >= 50") 308 - AssertEqual(t, int64(2), count, "Expected 2 books with progress >= 50") 309 }) 310 311 t.Run("Count books by rating", func(t *testing.T) { 312 count, err := repo.Count(ctx, BookListOptions{MinRating: 4.0}) 313 - AssertNoError(t, err, "Failed to count high-rated books") 314 - AssertEqual(t, int64(3), count, "Expected 3 books with rating >= 4.0") 315 }) 316 317 t.Run("Count with context cancellation", func(t *testing.T) { ··· 327 328 book := NewBookBuilder().WithTitle("Test Book").WithAuthor("Test Author").Build() 329 id, err := repo.Create(ctx, book) 330 - AssertNoError(t, err, "Failed to create book") 331 332 t.Run("Create with cancelled context", func(t *testing.T) { 333 newBook := NewBookBuilder().WithTitle("Cancelled").Build() ··· 399 400 t.Run("Get non-existent book", func(t *testing.T) { 401 _, err := repo.Get(ctx, 99999) 402 - AssertError(t, err, "Expected error for non-existent book") 403 }) 404 405 t.Run("Update non-existent book succeeds with no rows affected", func(t *testing.T) { 406 book := NewBookBuilder().WithTitle("Non-existent").Build() 407 book.ID = 99999 408 err := repo.Update(ctx, book) 409 - AssertNoError(t, err, "Update should not error when no rows affected") 410 }) 411 412 t.Run("Delete non-existent book succeeds with no rows affected", func(t *testing.T) { 413 err := repo.Delete(ctx, 99999) 414 - AssertNoError(t, err, "Delete should not error when no rows affected") 415 }) 416 417 t.Run("StartReading non-existent book", func(t *testing.T) { 418 err := repo.StartReading(ctx, 99999) 419 - AssertError(t, err, "Expected error for non-existent book") 420 }) 421 422 t.Run("FinishReading non-existent book", func(t *testing.T) { 423 err := repo.FinishReading(ctx, 99999) 424 - AssertError(t, err, "Expected error for non-existent book") 425 }) 426 427 t.Run("UpdateProgress non-existent book", func(t *testing.T) { 428 err := repo.UpdateProgress(ctx, 99999, 50) 429 - AssertError(t, err, "Expected error for non-existent book") 430 }) 431 432 t.Run("GetByAuthor with no results", func(t *testing.T) { 433 books, err := repo.GetByAuthor(ctx, "NonExistentAuthor") 434 - AssertNoError(t, err, "Should not error when no books found") 435 - AssertEqual(t, 0, len(books), "Expected empty result set") 436 }) 437 }) 438 }
··· 7 8 _ "github.com/mattn/go-sqlite3" 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 + "github.com/stormlightlabs/noteleaf/internal/shared" 11 ) 12 13 func TestBookRepository(t *testing.T) { ··· 20 book := CreateSampleBook() 21 22 id, err := repo.Create(ctx, book) 23 + shared.AssertNoError(t, err, "Failed to create book") 24 + shared.AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 25 + shared.AssertEqual(t, id, book.ID, "Expected book ID to be set correctly") 26 + shared.AssertFalse(t, book.Added.IsZero(), "Expected Added timestamp to be set") 27 }) 28 29 t.Run("Get Book", func(t *testing.T) { 30 original := CreateSampleBook() 31 id, err := repo.Create(ctx, original) 32 + shared.AssertNoError(t, err, "Failed to create book") 33 34 retrieved, err := repo.Get(ctx, id) 35 + shared.AssertNoError(t, err, "Failed to get book") 36 37 + shared.AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") 38 + shared.AssertEqual(t, original.Author, retrieved.Author, "Author mismatch") 39 + shared.AssertEqual(t, original.Status, retrieved.Status, "Status mismatch") 40 + shared.AssertEqual(t, original.Progress, retrieved.Progress, "Progress mismatch") 41 + shared.AssertEqual(t, original.Pages, retrieved.Pages, "Pages mismatch") 42 + shared.AssertEqual(t, original.Rating, retrieved.Rating, "Rating mismatch") 43 + shared.AssertEqual(t, original.Notes, retrieved.Notes, "Notes mismatch") 44 }) 45 46 t.Run("Update Book", func(t *testing.T) { 47 book := CreateSampleBook() 48 id, err := repo.Create(ctx, book) 49 + shared.AssertNoError(t, err, "Failed to create book") 50 51 book.Title = "Updated Book" 52 book.Status = "reading" ··· 56 book.Started = &now 57 58 err = repo.Update(ctx, book) 59 + shared.AssertNoError(t, err, "Failed to update book") 60 61 updated, err := repo.Get(ctx, id) 62 + shared.AssertNoError(t, err, "Failed to get updated book") 63 64 + shared.AssertEqual(t, "Updated Book", updated.Title, "Expected updated title") 65 + shared.AssertEqual(t, "reading", updated.Status, "Expected reading status") 66 + shared.AssertEqual(t, 50, updated.Progress, "Expected progress 50") 67 + shared.AssertEqual(t, 5.0, updated.Rating, "Expected rating 5.0") 68 + shared.AssertTrue(t, updated.Started != nil, "Expected started time to be set") 69 }) 70 71 t.Run("Delete Book", func(t *testing.T) { 72 book := CreateSampleBook() 73 id, err := repo.Create(ctx, book) 74 + shared.AssertNoError(t, err, "Failed to create book") 75 76 err = repo.Delete(ctx, id) 77 + shared.AssertNoError(t, err, "Failed to delete book") 78 79 _, err = repo.Get(ctx, id) 80 + shared.AssertError(t, err, "Expected error when getting deleted book") 81 }) 82 }) 83 ··· 95 96 for _, book := range books { 97 _, err := repo.Create(ctx, book) 98 + shared.AssertNoError(t, err, "Failed to create book") 99 } 100 101 t.Run("List All Books", func(t *testing.T) { 102 results, err := repo.List(ctx, BookListOptions{}) 103 + shared.AssertNoError(t, err, "Failed to list books") 104 + shared.AssertEqual(t, 4, len(results), "Expected 4 books") 105 }) 106 107 t.Run("List Books with Status Filter", func(t *testing.T) { 108 results, err := repo.List(ctx, BookListOptions{Status: "queued"}) 109 + shared.AssertNoError(t, err, "Failed to list books") 110 + shared.AssertEqual(t, 2, len(results), "Expected 2 queued books") 111 112 for _, book := range results { 113 + shared.AssertEqual(t, "queued", book.Status, "Expected queued status") 114 } 115 }) 116 117 t.Run("List Books by Author", func(t *testing.T) { 118 results, err := repo.List(ctx, BookListOptions{Author: "Author A"}) 119 + shared.AssertNoError(t, err, "Failed to list books") 120 + shared.AssertEqual(t, 2, len(results), "Expected 2 books by Author A") 121 122 for _, book := range results { 123 + shared.AssertEqual(t, "Author A", book.Author, "Expected author 'Author A'") 124 } 125 }) 126 127 t.Run("List Books with Progress Filter", func(t *testing.T) { 128 results, err := repo.List(ctx, BookListOptions{MinProgress: 50}) 129 + shared.AssertNoError(t, err, "Failed to list books") 130 + shared.AssertEqual(t, 2, len(results), "Expected 2 books with progress >= 50") 131 132 for _, book := range results { 133 + shared.AssertTrue(t, book.Progress >= 50, "Expected progress >= 50") 134 } 135 }) 136 137 t.Run("List Books with Rating Filter", func(t *testing.T) { 138 results, err := repo.List(ctx, BookListOptions{MinRating: 4.5}) 139 + shared.AssertNoError(t, err, "Failed to list books") 140 + shared.AssertEqual(t, 2, len(results), "Expected 2 books with rating >= 4.5") 141 142 for _, book := range results { 143 + shared.AssertTrue(t, book.Rating >= 4.5, "Expected rating >= 4.5") 144 } 145 }) 146 147 t.Run("List Books with Search", func(t *testing.T) { 148 results, err := repo.List(ctx, BookListOptions{Search: "Book 1"}) 149 + shared.AssertNoError(t, err, "Failed to list books") 150 + shared.AssertEqual(t, 1, len(results), "Expected 1 book matching search") 151 152 if len(results) > 0 { 153 + shared.AssertEqual(t, "Book 1", results[0].Title, "Expected 'Book 1'") 154 } 155 }) 156 157 t.Run("List Books with Limit", func(t *testing.T) { 158 results, err := repo.List(ctx, BookListOptions{Limit: 2}) 159 + shared.AssertNoError(t, err, "Failed to list books") 160 + shared.AssertEqual(t, 2, len(results), "Expected 2 books due to limit") 161 }) 162 }) 163 ··· 174 var book1ID int64 175 for _, book := range []*models.Book{book1, book2, book3, book4} { 176 id, err := repo.Create(ctx, book) 177 + shared.AssertNoError(t, err, "Failed to create book") 178 if book == book1 { 179 book1ID = id 180 } ··· 182 183 t.Run("GetQueued", func(t *testing.T) { 184 results, err := repo.GetQueued(ctx) 185 + shared.AssertNoError(t, err, "Failed to get queued books") 186 + shared.AssertEqual(t, 2, len(results), "Expected 2 queued books") 187 188 for _, book := range results { 189 + shared.AssertEqual(t, "queued", book.Status, "Expected queued status") 190 } 191 }) 192 193 t.Run("GetReading", func(t *testing.T) { 194 results, err := repo.GetReading(ctx) 195 + shared.AssertNoError(t, err, "Failed to get reading books") 196 + shared.AssertEqual(t, 1, len(results), "Expected 1 reading book") 197 198 if len(results) > 0 { 199 + shared.AssertEqual(t, "reading", results[0].Status, "Expected reading status") 200 } 201 }) 202 203 t.Run("GetFinished", func(t *testing.T) { 204 results, err := repo.GetFinished(ctx) 205 + shared.AssertNoError(t, err, "Failed to get finished books") 206 + shared.AssertEqual(t, 1, len(results), "Expected 1 finished book") 207 208 if len(results) > 0 { 209 + shared.AssertEqual(t, "finished", results[0].Status, "Expected finished status") 210 } 211 }) 212 213 t.Run("GetByAuthor", func(t *testing.T) { 214 results, err := repo.GetByAuthor(ctx, "Author A") 215 + shared.AssertNoError(t, err, "Failed to get books by author") 216 + shared.AssertEqual(t, 2, len(results), "Expected 2 books by Author A") 217 218 for _, book := range results { 219 + shared.AssertEqual(t, "Author A", book.Author, "Expected author 'Author A'") 220 } 221 }) 222 223 t.Run("StartReading", func(t *testing.T) { 224 err := repo.StartReading(ctx, book1ID) 225 + shared.AssertNoError(t, err, "Failed to start reading book") 226 227 updated, err := repo.Get(ctx, book1ID) 228 + shared.AssertNoError(t, err, "Failed to get updated book") 229 230 + shared.AssertEqual(t, "reading", updated.Status, "Expected status to be reading") 231 + shared.AssertTrue(t, updated.Started != nil, "Expected started timestamp to be set") 232 }) 233 234 t.Run("FinishReading", func(t *testing.T) { 235 newBook := &models.Book{Title: "New Book", Status: "reading", Progress: 80} 236 id, err := repo.Create(ctx, newBook) 237 + shared.AssertNoError(t, err, "Failed to create new book") 238 239 err = repo.FinishReading(ctx, id) 240 + shared.AssertNoError(t, err, "Failed to finish reading book") 241 242 updated, err := repo.Get(ctx, id) 243 + shared.AssertNoError(t, err, "Failed to get updated book") 244 245 + shared.AssertEqual(t, "finished", updated.Status, "Expected status to be finished") 246 + shared.AssertEqual(t, 100, updated.Progress, "Expected progress to be 100") 247 + shared.AssertTrue(t, updated.Finished != nil, "Expected finished timestamp to be set") 248 }) 249 250 t.Run("UpdateProgress", func(t *testing.T) { 251 newBook := &models.Book{Title: "Progress Book", Status: "queued", Progress: 0} 252 id, err := repo.Create(ctx, newBook) 253 + shared.AssertNoError(t, err, "Failed to create new book") 254 255 err = repo.UpdateProgress(ctx, id, 25) 256 + shared.AssertNoError(t, err, "Failed to update progress") 257 258 updated, err := repo.Get(ctx, id) 259 + shared.AssertNoError(t, err, "Failed to get updated book") 260 261 + shared.AssertEqual(t, "reading", updated.Status, "Expected status to be reading when progress > 0") 262 + shared.AssertEqual(t, 25, updated.Progress, "Expected progress 25") 263 + shared.AssertTrue(t, updated.Started != nil, "Expected started timestamp to be set when progress > 0") 264 265 err = repo.UpdateProgress(ctx, id, 100) 266 + shared.AssertNoError(t, err, "Failed to update progress to 100") 267 268 updated, err = repo.Get(ctx, id) 269 + shared.AssertNoError(t, err, "Failed to get updated book") 270 271 + shared.AssertEqual(t, "finished", updated.Status, "Expected status to be finished when progress = 100") 272 + shared.AssertEqual(t, 100, updated.Progress, "Expected progress 100") 273 + shared.AssertTrue(t, updated.Finished != nil, "Expected finished timestamp to be set when progress = 100") 274 }) 275 }) 276 ··· 288 289 for _, book := range books { 290 _, err := repo.Create(ctx, book) 291 + shared.AssertNoError(t, err, "Failed to create book") 292 } 293 294 t.Run("Count all books", func(t *testing.T) { 295 count, err := repo.Count(ctx, BookListOptions{}) 296 + shared.AssertNoError(t, err, "Failed to count books") 297 + shared.AssertEqual(t, int64(4), count, "Expected 4 books") 298 }) 299 300 t.Run("Count queued books", func(t *testing.T) { 301 count, err := repo.Count(ctx, BookListOptions{Status: "queued"}) 302 + shared.AssertNoError(t, err, "Failed to count queued books") 303 + shared.AssertEqual(t, int64(2), count, "Expected 2 queued books") 304 }) 305 306 t.Run("Count books by progress", func(t *testing.T) { 307 count, err := repo.Count(ctx, BookListOptions{MinProgress: 50}) 308 + shared.AssertNoError(t, err, "Failed to count books with progress >= 50") 309 + shared.AssertEqual(t, int64(2), count, "Expected 2 books with progress >= 50") 310 }) 311 312 t.Run("Count books by rating", func(t *testing.T) { 313 count, err := repo.Count(ctx, BookListOptions{MinRating: 4.0}) 314 + shared.AssertNoError(t, err, "Failed to count high-rated books") 315 + shared.AssertEqual(t, int64(3), count, "Expected 3 books with rating >= 4.0") 316 }) 317 318 t.Run("Count with context cancellation", func(t *testing.T) { ··· 328 329 book := NewBookBuilder().WithTitle("Test Book").WithAuthor("Test Author").Build() 330 id, err := repo.Create(ctx, book) 331 + shared.AssertNoError(t, err, "Failed to create book") 332 333 t.Run("Create with cancelled context", func(t *testing.T) { 334 newBook := NewBookBuilder().WithTitle("Cancelled").Build() ··· 400 401 t.Run("Get non-existent book", func(t *testing.T) { 402 _, err := repo.Get(ctx, 99999) 403 + shared.AssertError(t, err, "Expected error for non-existent book") 404 }) 405 406 t.Run("Update non-existent book succeeds with no rows affected", func(t *testing.T) { 407 book := NewBookBuilder().WithTitle("Non-existent").Build() 408 book.ID = 99999 409 err := repo.Update(ctx, book) 410 + shared.AssertNoError(t, err, "Update should not error when no rows affected") 411 }) 412 413 t.Run("Delete non-existent book succeeds with no rows affected", func(t *testing.T) { 414 err := repo.Delete(ctx, 99999) 415 + shared.AssertNoError(t, err, "Delete should not error when no rows affected") 416 }) 417 418 t.Run("StartReading non-existent book", func(t *testing.T) { 419 err := repo.StartReading(ctx, 99999) 420 + shared.AssertError(t, err, "Expected error for non-existent book") 421 }) 422 423 t.Run("FinishReading non-existent book", func(t *testing.T) { 424 err := repo.FinishReading(ctx, 99999) 425 + shared.AssertError(t, err, "Expected error for non-existent book") 426 }) 427 428 t.Run("UpdateProgress non-existent book", func(t *testing.T) { 429 err := repo.UpdateProgress(ctx, 99999, 50) 430 + shared.AssertError(t, err, "Expected error for non-existent book") 431 }) 432 433 t.Run("GetByAuthor with no results", func(t *testing.T) { 434 books, err := repo.GetByAuthor(ctx, "NonExistentAuthor") 435 + shared.AssertNoError(t, err, "Should not error when no books found") 436 + shared.AssertEqual(t, 0, len(books), "Expected empty result set") 437 }) 438 }) 439 }
+73 -71
internal/repo/find_methods_test.go
··· 3 import ( 4 "context" 5 "testing" 6 ) 7 8 func TestFindMethods(t *testing.T) { ··· 16 Status: "pending", 17 } 18 tasks, err := repos.Tasks.Find(ctx, options) 19 - AssertNoError(t, err, "Find should succeed") 20 - AssertTrue(t, len(tasks) >= 1, "Should find at least one pending task") 21 for _, task := range tasks { 22 - AssertEqual(t, "pending", task.Status, "All returned tasks should be pending") 23 } 24 }) 25 ··· 28 Priority: "high", 29 } 30 tasks, err := repos.Tasks.Find(ctx, options) 31 - AssertNoError(t, err, "Find should succeed") 32 - AssertTrue(t, len(tasks) >= 1, "Should find at least one high priority task") 33 for _, task := range tasks { 34 - AssertEqual(t, "high", task.Priority, "All returned tasks should be high priority") 35 } 36 }) 37 ··· 40 Project: "test-project", 41 } 42 tasks, err := repos.Tasks.Find(ctx, options) 43 - AssertNoError(t, err, "Find should succeed") 44 - AssertTrue(t, len(tasks) >= 1, "Should find tasks in test-project") 45 for _, task := range tasks { 46 - AssertEqual(t, "test-project", task.Project, "All returned tasks should be in test-project") 47 } 48 }) 49 ··· 52 Context: "test-context", 53 } 54 tasks, err := repos.Tasks.Find(ctx, options) 55 - AssertNoError(t, err, "Find should succeed") 56 - AssertTrue(t, len(tasks) >= 1, "Should find tasks in test-context") 57 for _, task := range tasks { 58 - AssertEqual(t, "test-context", task.Context, "All returned tasks should be in test-context") 59 } 60 }) 61 ··· 66 Project: "test-project", 67 } 68 tasks, err := repos.Tasks.Find(ctx, options) 69 - AssertNoError(t, err, "Find should succeed") 70 for _, task := range tasks { 71 - AssertEqual(t, "pending", task.Status, "Task should be pending") 72 - AssertEqual(t, "high", task.Priority, "Task should be high priority") 73 - AssertEqual(t, "test-project", task.Project, "Task should be in test-project") 74 } 75 }) 76 ··· 79 Status: "non-existent-status", 80 } 81 tasks, err := repos.Tasks.Find(ctx, options) 82 - AssertNoError(t, err, "Find should succeed even with no results") 83 - AssertEqual(t, 0, len(tasks), "Should find no tasks") 84 }) 85 86 t.Run("returns all tasks with empty options", func(t *testing.T) { 87 options := TaskListOptions{} 88 tasks, err := repos.Tasks.Find(ctx, options) 89 - AssertNoError(t, err, "Find should succeed with empty options") 90 - AssertTrue(t, len(tasks) >= 2, "Should return all tasks for empty options") 91 }) 92 }) 93 ··· 97 Status: "reading", 98 } 99 books, err := repos.Books.Find(ctx, options) 100 - AssertNoError(t, err, "Find should succeed") 101 - AssertTrue(t, len(books) >= 1, "Should find at least one book being read") 102 for _, book := range books { 103 - AssertEqual(t, "reading", book.Status, "All returned books should be reading") 104 } 105 }) 106 ··· 109 Author: "Test Author", 110 } 111 books, err := repos.Books.Find(ctx, options) 112 - AssertNoError(t, err, "Find should succeed") 113 - AssertTrue(t, len(books) >= 1, "Should find at least one book by Test Author") 114 for _, book := range books { 115 - AssertEqual(t, "Test Author", book.Author, "All returned books should be by Test Author") 116 } 117 }) 118 ··· 121 MinProgress: 0, 122 } 123 books, err := repos.Books.Find(ctx, options) 124 - AssertNoError(t, err, "Find should succeed") 125 - AssertTrue(t, len(books) >= 1, "Should find books with progress >= 0") 126 for _, book := range books { 127 - AssertTrue(t, book.Progress >= 0, "All returned books should have progress >= 0") 128 } 129 }) 130 ··· 135 MinProgress: 0, 136 } 137 books, err := repos.Books.Find(ctx, options) 138 - AssertNoError(t, err, "Find should succeed") 139 for _, book := range books { 140 - AssertEqual(t, "reading", book.Status, "Book should be reading") 141 - AssertEqual(t, "Test Author", book.Author, "Book should be by Test Author") 142 - AssertTrue(t, book.Progress >= 0, "Book should have progress >= 0") 143 } 144 }) 145 ··· 148 Status: "non-existent-status", 149 } 150 books, err := repos.Books.Find(ctx, options) 151 - AssertNoError(t, err, "Find should succeed even with no results") 152 - AssertEqual(t, 0, len(books), "Should find no books") 153 }) 154 155 t.Run("returns all books with empty options", func(t *testing.T) { 156 options := BookListOptions{} 157 books, err := repos.Books.Find(ctx, options) 158 - AssertNoError(t, err, "Find should succeed with empty options") 159 - AssertTrue(t, len(books) >= 2, "Should return all books for empty options") 160 }) 161 }) 162 ··· 166 Status: "watched", 167 } 168 movies, err := repos.Movies.Find(ctx, options) 169 - AssertNoError(t, err, "Find should succeed") 170 - AssertTrue(t, len(movies) >= 1, "Should find at least one watched movie") 171 for _, movie := range movies { 172 - AssertEqual(t, "watched", movie.Status, "All returned movies should be watched") 173 } 174 }) 175 ··· 178 Year: 2023, 179 } 180 movies, err := repos.Movies.Find(ctx, options) 181 - AssertNoError(t, err, "Find should succeed") 182 - AssertTrue(t, len(movies) >= 1, "Should find movies from 2023") 183 for _, movie := range movies { 184 - AssertEqual(t, 2023, movie.Year, "Movie should be from 2023") 185 } 186 }) 187 ··· 190 MinRating: 0.0, 191 } 192 movies, err := repos.Movies.Find(ctx, options) 193 - AssertNoError(t, err, "Find should succeed") 194 - AssertTrue(t, len(movies) >= 1, "Should find movies with rating >= 0") 195 for _, movie := range movies { 196 - AssertTrue(t, movie.Rating >= 0.0, "Movie rating should be >= 0") 197 } 198 }) 199 ··· 204 MinRating: 0.0, 205 } 206 movies, err := repos.Movies.Find(ctx, options) 207 - AssertNoError(t, err, "Find should succeed") 208 for _, movie := range movies { 209 - AssertEqual(t, "watched", movie.Status, "Movie should be watched") 210 - AssertEqual(t, 2023, movie.Year, "Movie should be from 2023") 211 - AssertTrue(t, movie.Rating >= 0.0, "Movie rating should be >= 0") 212 } 213 }) 214 ··· 217 Status: "non-existent-status", 218 } 219 movies, err := repos.Movies.Find(ctx, options) 220 - AssertNoError(t, err, "Find should succeed even with no results") 221 - AssertEqual(t, 0, len(movies), "Should find no movies") 222 }) 223 224 t.Run("returns all movies with empty options", func(t *testing.T) { 225 options := MovieListOptions{} 226 movies, err := repos.Movies.Find(ctx, options) 227 - AssertNoError(t, err, "Find should succeed with empty options") 228 - AssertTrue(t, len(movies) >= 2, "Should return all movies for empty options") 229 }) 230 }) 231 ··· 235 Status: "watching", 236 } 237 shows, err := repos.TV.Find(ctx, options) 238 - AssertNoError(t, err, "Find should succeed") 239 - AssertTrue(t, len(shows) >= 1, "Should find at least one TV show being watched") 240 for _, show := range shows { 241 - AssertEqual(t, "watching", show.Status, "All returned shows should be watching") 242 } 243 }) 244 ··· 247 Season: 1, 248 } 249 shows, err := repos.TV.Find(ctx, options) 250 - AssertNoError(t, err, "Find should succeed") 251 - AssertTrue(t, len(shows) >= 1, "Should find TV shows with season 1") 252 for _, show := range shows { 253 - AssertEqual(t, 1, show.Season, "All returned shows should be season 1") 254 } 255 }) 256 ··· 259 MinRating: 0.0, 260 } 261 shows, err := repos.TV.Find(ctx, options) 262 - AssertNoError(t, err, "Find should succeed") 263 - AssertTrue(t, len(shows) >= 1, "Should find TV shows with rating >= 0") 264 for _, show := range shows { 265 - AssertTrue(t, show.Rating >= 0.0, "Show rating should be >= 0") 266 } 267 }) 268 ··· 273 MinRating: 0.0, 274 } 275 shows, err := repos.TV.Find(ctx, options) 276 - AssertNoError(t, err, "Find should succeed") 277 for _, show := range shows { 278 - AssertEqual(t, "watching", show.Status, "Show should be watching") 279 - AssertEqual(t, 1, show.Season, "Show should be season 1") 280 - AssertTrue(t, show.Rating >= 0.0, "Show rating should be >= 0") 281 } 282 }) 283 ··· 286 Status: "non-existent-status", 287 } 288 shows, err := repos.TV.Find(ctx, options) 289 - AssertNoError(t, err, "Find should succeed even with no results") 290 - AssertEqual(t, 0, len(shows), "Should find no TV shows") 291 }) 292 293 t.Run("returns all TV shows with empty options", func(t *testing.T) { 294 options := TVListOptions{} 295 shows, err := repos.TV.Find(ctx, options) 296 - AssertNoError(t, err, "Find should succeed with empty options") 297 - AssertTrue(t, len(shows) >= 2, "Should return all TV shows for empty options") 298 }) 299 }) 300 }
··· 3 import ( 4 "context" 5 "testing" 6 + 7 + "github.com/stormlightlabs/noteleaf/internal/shared" 8 ) 9 10 func TestFindMethods(t *testing.T) { ··· 18 Status: "pending", 19 } 20 tasks, err := repos.Tasks.Find(ctx, options) 21 + shared.AssertNoError(t, err, "Find should succeed") 22 + shared.AssertTrue(t, len(tasks) >= 1, "Should find at least one pending task") 23 for _, task := range tasks { 24 + shared.AssertEqual(t, "pending", task.Status, "All returned tasks should be pending") 25 } 26 }) 27 ··· 30 Priority: "high", 31 } 32 tasks, err := repos.Tasks.Find(ctx, options) 33 + shared.AssertNoError(t, err, "Find should succeed") 34 + shared.AssertTrue(t, len(tasks) >= 1, "Should find at least one high priority task") 35 for _, task := range tasks { 36 + shared.AssertEqual(t, "high", task.Priority, "All returned tasks should be high priority") 37 } 38 }) 39 ··· 42 Project: "test-project", 43 } 44 tasks, err := repos.Tasks.Find(ctx, options) 45 + shared.AssertNoError(t, err, "Find should succeed") 46 + shared.AssertTrue(t, len(tasks) >= 1, "Should find tasks in test-project") 47 for _, task := range tasks { 48 + shared.AssertEqual(t, "test-project", task.Project, "All returned tasks should be in test-project") 49 } 50 }) 51 ··· 54 Context: "test-context", 55 } 56 tasks, err := repos.Tasks.Find(ctx, options) 57 + shared.AssertNoError(t, err, "Find should succeed") 58 + shared.AssertTrue(t, len(tasks) >= 1, "Should find tasks in test-context") 59 for _, task := range tasks { 60 + shared.AssertEqual(t, "test-context", task.Context, "All returned tasks should be in test-context") 61 } 62 }) 63 ··· 68 Project: "test-project", 69 } 70 tasks, err := repos.Tasks.Find(ctx, options) 71 + shared.AssertNoError(t, err, "Find should succeed") 72 for _, task := range tasks { 73 + shared.AssertEqual(t, "pending", task.Status, "Task should be pending") 74 + shared.AssertEqual(t, "high", task.Priority, "Task should be high priority") 75 + shared.AssertEqual(t, "test-project", task.Project, "Task should be in test-project") 76 } 77 }) 78 ··· 81 Status: "non-existent-status", 82 } 83 tasks, err := repos.Tasks.Find(ctx, options) 84 + shared.AssertNoError(t, err, "Find should succeed even with no results") 85 + shared.AssertEqual(t, 0, len(tasks), "Should find no tasks") 86 }) 87 88 t.Run("returns all tasks with empty options", func(t *testing.T) { 89 options := TaskListOptions{} 90 tasks, err := repos.Tasks.Find(ctx, options) 91 + shared.AssertNoError(t, err, "Find should succeed with empty options") 92 + shared.AssertTrue(t, len(tasks) >= 2, "Should return all tasks for empty options") 93 }) 94 }) 95 ··· 99 Status: "reading", 100 } 101 books, err := repos.Books.Find(ctx, options) 102 + shared.AssertNoError(t, err, "Find should succeed") 103 + shared.AssertTrue(t, len(books) >= 1, "Should find at least one book being read") 104 for _, book := range books { 105 + shared.AssertEqual(t, "reading", book.Status, "All returned books should be reading") 106 } 107 }) 108 ··· 111 Author: "Test Author", 112 } 113 books, err := repos.Books.Find(ctx, options) 114 + shared.AssertNoError(t, err, "Find should succeed") 115 + shared.AssertTrue(t, len(books) >= 1, "Should find at least one book by Test Author") 116 for _, book := range books { 117 + shared.AssertEqual(t, "Test Author", book.Author, "All returned books should be by Test Author") 118 } 119 }) 120 ··· 123 MinProgress: 0, 124 } 125 books, err := repos.Books.Find(ctx, options) 126 + shared.AssertNoError(t, err, "Find should succeed") 127 + shared.AssertTrue(t, len(books) >= 1, "Should find books with progress >= 0") 128 for _, book := range books { 129 + shared.AssertTrue(t, book.Progress >= 0, "All returned books should have progress >= 0") 130 } 131 }) 132 ··· 137 MinProgress: 0, 138 } 139 books, err := repos.Books.Find(ctx, options) 140 + shared.AssertNoError(t, err, "Find should succeed") 141 for _, book := range books { 142 + shared.AssertEqual(t, "reading", book.Status, "Book should be reading") 143 + shared.AssertEqual(t, "Test Author", book.Author, "Book should be by Test Author") 144 + shared.AssertTrue(t, book.Progress >= 0, "Book should have progress >= 0") 145 } 146 }) 147 ··· 150 Status: "non-existent-status", 151 } 152 books, err := repos.Books.Find(ctx, options) 153 + shared.AssertNoError(t, err, "Find should succeed even with no results") 154 + shared.AssertEqual(t, 0, len(books), "Should find no books") 155 }) 156 157 t.Run("returns all books with empty options", func(t *testing.T) { 158 options := BookListOptions{} 159 books, err := repos.Books.Find(ctx, options) 160 + shared.AssertNoError(t, err, "Find should succeed with empty options") 161 + shared.AssertTrue(t, len(books) >= 2, "Should return all books for empty options") 162 }) 163 }) 164 ··· 168 Status: "watched", 169 } 170 movies, err := repos.Movies.Find(ctx, options) 171 + shared.AssertNoError(t, err, "Find should succeed") 172 + shared.AssertTrue(t, len(movies) >= 1, "Should find at least one watched movie") 173 for _, movie := range movies { 174 + shared.AssertEqual(t, "watched", movie.Status, "All returned movies should be watched") 175 } 176 }) 177 ··· 180 Year: 2023, 181 } 182 movies, err := repos.Movies.Find(ctx, options) 183 + shared.AssertNoError(t, err, "Find should succeed") 184 + shared.AssertTrue(t, len(movies) >= 1, "Should find movies from 2023") 185 for _, movie := range movies { 186 + shared.AssertEqual(t, 2023, movie.Year, "Movie should be from 2023") 187 } 188 }) 189 ··· 192 MinRating: 0.0, 193 } 194 movies, err := repos.Movies.Find(ctx, options) 195 + shared.AssertNoError(t, err, "Find should succeed") 196 + shared.AssertTrue(t, len(movies) >= 1, "Should find movies with rating >= 0") 197 for _, movie := range movies { 198 + shared.AssertTrue(t, movie.Rating >= 0.0, "Movie rating should be >= 0") 199 } 200 }) 201 ··· 206 MinRating: 0.0, 207 } 208 movies, err := repos.Movies.Find(ctx, options) 209 + shared.AssertNoError(t, err, "Find should succeed") 210 for _, movie := range movies { 211 + shared.AssertEqual(t, "watched", movie.Status, "Movie should be watched") 212 + shared.AssertEqual(t, 2023, movie.Year, "Movie should be from 2023") 213 + shared.AssertTrue(t, movie.Rating >= 0.0, "Movie rating should be >= 0") 214 } 215 }) 216 ··· 219 Status: "non-existent-status", 220 } 221 movies, err := repos.Movies.Find(ctx, options) 222 + shared.AssertNoError(t, err, "Find should succeed even with no results") 223 + shared.AssertEqual(t, 0, len(movies), "Should find no movies") 224 }) 225 226 t.Run("returns all movies with empty options", func(t *testing.T) { 227 options := MovieListOptions{} 228 movies, err := repos.Movies.Find(ctx, options) 229 + shared.AssertNoError(t, err, "Find should succeed with empty options") 230 + shared.AssertTrue(t, len(movies) >= 2, "Should return all movies for empty options") 231 }) 232 }) 233 ··· 237 Status: "watching", 238 } 239 shows, err := repos.TV.Find(ctx, options) 240 + shared.AssertNoError(t, err, "Find should succeed") 241 + shared.AssertTrue(t, len(shows) >= 1, "Should find at least one TV show being watched") 242 for _, show := range shows { 243 + shared.AssertEqual(t, "watching", show.Status, "All returned shows should be watching") 244 } 245 }) 246 ··· 249 Season: 1, 250 } 251 shows, err := repos.TV.Find(ctx, options) 252 + shared.AssertNoError(t, err, "Find should succeed") 253 + shared.AssertTrue(t, len(shows) >= 1, "Should find TV shows with season 1") 254 for _, show := range shows { 255 + shared.AssertEqual(t, 1, show.Season, "All returned shows should be season 1") 256 } 257 }) 258 ··· 261 MinRating: 0.0, 262 } 263 shows, err := repos.TV.Find(ctx, options) 264 + shared.AssertNoError(t, err, "Find should succeed") 265 + shared.AssertTrue(t, len(shows) >= 1, "Should find TV shows with rating >= 0") 266 for _, show := range shows { 267 + shared.AssertTrue(t, show.Rating >= 0.0, "Show rating should be >= 0") 268 } 269 }) 270 ··· 275 MinRating: 0.0, 276 } 277 shows, err := repos.TV.Find(ctx, options) 278 + shared.AssertNoError(t, err, "Find should succeed") 279 for _, show := range shows { 280 + shared.AssertEqual(t, "watching", show.Status, "Show should be watching") 281 + shared.AssertEqual(t, 1, show.Season, "Show should be season 1") 282 + shared.AssertTrue(t, show.Rating >= 0.0, "Show rating should be >= 0") 283 } 284 }) 285 ··· 288 Status: "non-existent-status", 289 } 290 shows, err := repos.TV.Find(ctx, options) 291 + shared.AssertNoError(t, err, "Find should succeed even with no results") 292 + shared.AssertEqual(t, 0, len(shows), "Should find no TV shows") 293 }) 294 295 t.Run("returns all TV shows with empty options", func(t *testing.T) { 296 options := TVListOptions{} 297 shows, err := repos.TV.Find(ctx, options) 298 + shared.AssertNoError(t, err, "Find should succeed with empty options") 299 + shared.AssertTrue(t, len(shows) >= 2, "Should return all TV shows for empty options") 300 }) 301 }) 302 }
+64 -63
internal/repo/movie_repository_test.go
··· 7 8 _ "github.com/mattn/go-sqlite3" 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 ) 11 12 func TestMovieRepository(t *testing.T) { ··· 19 movie := CreateSampleMovie() 20 21 id, err := repo.Create(ctx, movie) 22 - AssertNoError(t, err, "Failed to create movie") 23 - AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 24 - AssertEqual(t, id, movie.ID, "Expected movie ID to be set correctly") 25 - AssertFalse(t, movie.Added.IsZero(), "Expected Added timestamp to be set") 26 }) 27 28 t.Run("Get Movie", func(t *testing.T) { 29 original := CreateSampleMovie() 30 id, err := repo.Create(ctx, original) 31 - AssertNoError(t, err, "Failed to create movie") 32 33 retrieved, err := repo.Get(ctx, id) 34 - AssertNoError(t, err, "Failed to get movie") 35 36 - AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") 37 - AssertEqual(t, original.Year, retrieved.Year, "Year mismatch") 38 - AssertEqual(t, original.Status, retrieved.Status, "Status mismatch") 39 - AssertEqual(t, original.Rating, retrieved.Rating, "Rating mismatch") 40 - AssertEqual(t, original.Notes, retrieved.Notes, "Notes mismatch") 41 }) 42 43 t.Run("Update Movie", func(t *testing.T) { 44 movie := CreateSampleMovie() 45 id, err := repo.Create(ctx, movie) 46 - AssertNoError(t, err, "Failed to create movie") 47 48 movie.Title = "Updated Movie" 49 movie.Status = "watched" ··· 52 movie.Watched = &now 53 54 err = repo.Update(ctx, movie) 55 - AssertNoError(t, err, "Failed to update movie") 56 57 updated, err := repo.Get(ctx, id) 58 - AssertNoError(t, err, "Failed to get updated movie") 59 60 - AssertEqual(t, "Updated Movie", updated.Title, "Expected updated title") 61 - AssertEqual(t, "watched", updated.Status, "Expected watched status") 62 - AssertEqual(t, 9.0, updated.Rating, "Expected rating 9.0") 63 - AssertTrue(t, updated.Watched != nil, "Expected watched time to be set") 64 }) 65 66 t.Run("Delete Movie", func(t *testing.T) { 67 movie := CreateSampleMovie() 68 id, err := repo.Create(ctx, movie) 69 - AssertNoError(t, err, "Failed to create movie") 70 71 err = repo.Delete(ctx, id) 72 - AssertNoError(t, err, "Failed to delete movie") 73 74 _, err = repo.Get(ctx, id) 75 - AssertError(t, err, "Expected error when getting deleted movie") 76 }) 77 }) 78 ··· 89 90 for _, movie := range movies { 91 _, err := repo.Create(ctx, movie) 92 - AssertNoError(t, err, "Failed to create movie") 93 } 94 95 t.Run("List All Movies", func(t *testing.T) { 96 results, err := repo.List(ctx, MovieListOptions{}) 97 - AssertNoError(t, err, "Failed to list movies") 98 - AssertEqual(t, 3, len(results), "Expected 3 movies") 99 }) 100 101 t.Run("List Movies with Status Filter", func(t *testing.T) { 102 results, err := repo.List(ctx, MovieListOptions{Status: "queued"}) 103 - AssertNoError(t, err, "Failed to list movies") 104 - AssertEqual(t, 2, len(results), "Expected 2 queued movies") 105 106 for _, movie := range results { 107 - AssertEqual(t, "queued", movie.Status, "Expected queued status") 108 } 109 }) 110 111 t.Run("List Movies with Year Filter", func(t *testing.T) { 112 results, err := repo.List(ctx, MovieListOptions{Year: 2021}) 113 - AssertNoError(t, err, "Failed to list movies") 114 - AssertEqual(t, 1, len(results), "Expected 1 movie from 2021") 115 116 if len(results) > 0 { 117 - AssertEqual(t, 2021, results[0].Year, "Expected year 2021") 118 } 119 }) 120 121 t.Run("List Movies with Rating Filter", func(t *testing.T) { 122 results, err := repo.List(ctx, MovieListOptions{MinRating: 8.0}) 123 - AssertNoError(t, err, "Failed to list movies") 124 - AssertEqual(t, 2, len(results), "Expected 2 movies with rating >= 8.0") 125 126 for _, movie := range results { 127 - AssertTrue(t, movie.Rating >= 8.0, "Expected rating >= 8.0") 128 } 129 }) 130 131 t.Run("List Movies with Search", func(t *testing.T) { 132 results, err := repo.List(ctx, MovieListOptions{Search: "Movie 1"}) 133 - AssertNoError(t, err, "Failed to list movies") 134 - AssertEqual(t, 1, len(results), "Expected 1 movie matching search") 135 136 if len(results) > 0 { 137 - AssertEqual(t, "Movie 1", results[0].Title, "Expected 'Movie 1'") 138 } 139 }) 140 141 t.Run("List Movies with Limit", func(t *testing.T) { 142 results, err := repo.List(ctx, MovieListOptions{Limit: 2}) 143 - AssertNoError(t, err, "Failed to list movies") 144 - AssertEqual(t, 2, len(results), "Expected 2 movies due to limit") 145 }) 146 }) 147 ··· 157 var movie1ID int64 158 for _, movie := range []*models.Movie{movie1, movie2, movie3} { 159 id, err := repo.Create(ctx, movie) 160 - AssertNoError(t, err, "Failed to create movie") 161 if movie == movie1 { 162 movie1ID = id 163 } ··· 165 166 t.Run("GetQueued", func(t *testing.T) { 167 results, err := repo.GetQueued(ctx) 168 - AssertNoError(t, err, "Failed to get queued movies") 169 - AssertEqual(t, 2, len(results), "Expected 2 queued movies") 170 171 for _, movie := range results { 172 - AssertEqual(t, "queued", movie.Status, "Expected queued status") 173 } 174 }) 175 176 t.Run("GetWatched", func(t *testing.T) { 177 results, err := repo.GetWatched(ctx) 178 - AssertNoError(t, err, "Failed to get watched movies") 179 - AssertEqual(t, 1, len(results), "Expected 1 watched movie") 180 181 if len(results) > 0 { 182 - AssertEqual(t, "watched", results[0].Status, "Expected watched status") 183 } 184 }) 185 186 t.Run("MarkWatched", func(t *testing.T) { 187 err := repo.MarkWatched(ctx, movie1ID) 188 - AssertNoError(t, err, "Failed to mark movie as watched") 189 190 updated, err := repo.Get(ctx, movie1ID) 191 - AssertNoError(t, err, "Failed to get updated movie") 192 193 - AssertEqual(t, "watched", updated.Status, "Expected status to be watched") 194 - AssertTrue(t, updated.Watched != nil, "Expected watched timestamp to be set") 195 }) 196 }) 197 ··· 208 209 for _, movie := range movies { 210 _, err := repo.Create(ctx, movie) 211 - AssertNoError(t, err, "Failed to create movie") 212 } 213 214 t.Run("Count all movies", func(t *testing.T) { 215 count, err := repo.Count(ctx, MovieListOptions{}) 216 - AssertNoError(t, err, "Failed to count movies") 217 - AssertEqual(t, int64(3), count, "Expected 3 movies") 218 }) 219 220 t.Run("Count queued movies", func(t *testing.T) { 221 count, err := repo.Count(ctx, MovieListOptions{Status: "queued"}) 222 - AssertNoError(t, err, "Failed to count queued movies") 223 - AssertEqual(t, int64(2), count, "Expected 2 queued movies") 224 }) 225 226 t.Run("Count movies by rating", func(t *testing.T) { 227 count, err := repo.Count(ctx, MovieListOptions{MinRating: 8.0}) 228 - AssertNoError(t, err, "Failed to count high-rated movies") 229 - AssertEqual(t, int64(2), count, "Expected 2 movies with rating >= 8.0") 230 }) 231 232 t.Run("Count with context cancellation", func(t *testing.T) { ··· 242 243 movie := NewMovieBuilder().WithTitle("Test Movie").WithYear(2023).Build() 244 id, err := repo.Create(ctx, movie) 245 - AssertNoError(t, err, "Failed to create movie") 246 247 t.Run("Create with cancelled context", func(t *testing.T) { 248 newMovie := NewMovieBuilder().WithTitle("Cancelled").Build() ··· 294 295 t.Run("Get non-existent movie", func(t *testing.T) { 296 _, err := repo.Get(ctx, 99999) 297 - AssertError(t, err, "Expected error for non-existent movie") 298 }) 299 300 t.Run("Update non-existent movie succeeds with no rows affected", func(t *testing.T) { 301 movie := NewMovieBuilder().WithTitle("Non-existent").Build() 302 movie.ID = 99999 303 err := repo.Update(ctx, movie) 304 - AssertNoError(t, err, "Update should not error when no rows affected") 305 }) 306 307 t.Run("Delete non-existent movie succeeds with no rows affected", func(t *testing.T) { 308 err := repo.Delete(ctx, 99999) 309 - AssertNoError(t, err, "Delete should not error when no rows affected") 310 }) 311 312 t.Run("MarkWatched non-existent movie", func(t *testing.T) { 313 err := repo.MarkWatched(ctx, 99999) 314 - AssertError(t, err, "Expected error for non-existent movie") 315 }) 316 317 t.Run("List with no results", func(t *testing.T) { 318 movies, err := repo.List(ctx, MovieListOptions{Year: 1900}) 319 - AssertNoError(t, err, "Should not error when no movies found") 320 - AssertEqual(t, 0, len(movies), "Expected empty result set") 321 }) 322 }) 323 }
··· 7 8 _ "github.com/mattn/go-sqlite3" 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 + "github.com/stormlightlabs/noteleaf/internal/shared" 11 ) 12 13 func TestMovieRepository(t *testing.T) { ··· 20 movie := CreateSampleMovie() 21 22 id, err := repo.Create(ctx, movie) 23 + shared.AssertNoError(t, err, "Failed to create movie") 24 + shared.AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 25 + shared.AssertEqual(t, id, movie.ID, "Expected movie ID to be set correctly") 26 + shared.AssertFalse(t, movie.Added.IsZero(), "Expected Added timestamp to be set") 27 }) 28 29 t.Run("Get Movie", func(t *testing.T) { 30 original := CreateSampleMovie() 31 id, err := repo.Create(ctx, original) 32 + shared.AssertNoError(t, err, "Failed to create movie") 33 34 retrieved, err := repo.Get(ctx, id) 35 + shared.AssertNoError(t, err, "Failed to get movie") 36 37 + shared.AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") 38 + shared.AssertEqual(t, original.Year, retrieved.Year, "Year mismatch") 39 + shared.AssertEqual(t, original.Status, retrieved.Status, "Status mismatch") 40 + shared.AssertEqual(t, original.Rating, retrieved.Rating, "Rating mismatch") 41 + shared.AssertEqual(t, original.Notes, retrieved.Notes, "Notes mismatch") 42 }) 43 44 t.Run("Update Movie", func(t *testing.T) { 45 movie := CreateSampleMovie() 46 id, err := repo.Create(ctx, movie) 47 + shared.AssertNoError(t, err, "Failed to create movie") 48 49 movie.Title = "Updated Movie" 50 movie.Status = "watched" ··· 53 movie.Watched = &now 54 55 err = repo.Update(ctx, movie) 56 + shared.AssertNoError(t, err, "Failed to update movie") 57 58 updated, err := repo.Get(ctx, id) 59 + shared.AssertNoError(t, err, "Failed to get updated movie") 60 61 + shared.AssertEqual(t, "Updated Movie", updated.Title, "Expected updated title") 62 + shared.AssertEqual(t, "watched", updated.Status, "Expected watched status") 63 + shared.AssertEqual(t, 9.0, updated.Rating, "Expected rating 9.0") 64 + shared.AssertTrue(t, updated.Watched != nil, "Expected watched time to be set") 65 }) 66 67 t.Run("Delete Movie", func(t *testing.T) { 68 movie := CreateSampleMovie() 69 id, err := repo.Create(ctx, movie) 70 + shared.AssertNoError(t, err, "Failed to create movie") 71 72 err = repo.Delete(ctx, id) 73 + shared.AssertNoError(t, err, "Failed to delete movie") 74 75 _, err = repo.Get(ctx, id) 76 + shared.AssertError(t, err, "Expected error when getting deleted movie") 77 }) 78 }) 79 ··· 90 91 for _, movie := range movies { 92 _, err := repo.Create(ctx, movie) 93 + shared.AssertNoError(t, err, "Failed to create movie") 94 } 95 96 t.Run("List All Movies", func(t *testing.T) { 97 results, err := repo.List(ctx, MovieListOptions{}) 98 + shared.AssertNoError(t, err, "Failed to list movies") 99 + shared.AssertEqual(t, 3, len(results), "Expected 3 movies") 100 }) 101 102 t.Run("List Movies with Status Filter", func(t *testing.T) { 103 results, err := repo.List(ctx, MovieListOptions{Status: "queued"}) 104 + shared.AssertNoError(t, err, "Failed to list movies") 105 + shared.AssertEqual(t, 2, len(results), "Expected 2 queued movies") 106 107 for _, movie := range results { 108 + shared.AssertEqual(t, "queued", movie.Status, "Expected queued status") 109 } 110 }) 111 112 t.Run("List Movies with Year Filter", func(t *testing.T) { 113 results, err := repo.List(ctx, MovieListOptions{Year: 2021}) 114 + shared.AssertNoError(t, err, "Failed to list movies") 115 + shared.AssertEqual(t, 1, len(results), "Expected 1 movie from 2021") 116 117 if len(results) > 0 { 118 + shared.AssertEqual(t, 2021, results[0].Year, "Expected year 2021") 119 } 120 }) 121 122 t.Run("List Movies with Rating Filter", func(t *testing.T) { 123 results, err := repo.List(ctx, MovieListOptions{MinRating: 8.0}) 124 + shared.AssertNoError(t, err, "Failed to list movies") 125 + shared.AssertEqual(t, 2, len(results), "Expected 2 movies with rating >= 8.0") 126 127 for _, movie := range results { 128 + shared.AssertTrue(t, movie.Rating >= 8.0, "Expected rating >= 8.0") 129 } 130 }) 131 132 t.Run("List Movies with Search", func(t *testing.T) { 133 results, err := repo.List(ctx, MovieListOptions{Search: "Movie 1"}) 134 + shared.AssertNoError(t, err, "Failed to list movies") 135 + shared.AssertEqual(t, 1, len(results), "Expected 1 movie matching search") 136 137 if len(results) > 0 { 138 + shared.AssertEqual(t, "Movie 1", results[0].Title, "Expected 'Movie 1'") 139 } 140 }) 141 142 t.Run("List Movies with Limit", func(t *testing.T) { 143 results, err := repo.List(ctx, MovieListOptions{Limit: 2}) 144 + shared.AssertNoError(t, err, "Failed to list movies") 145 + shared.AssertEqual(t, 2, len(results), "Expected 2 movies due to limit") 146 }) 147 }) 148 ··· 158 var movie1ID int64 159 for _, movie := range []*models.Movie{movie1, movie2, movie3} { 160 id, err := repo.Create(ctx, movie) 161 + shared.AssertNoError(t, err, "Failed to create movie") 162 if movie == movie1 { 163 movie1ID = id 164 } ··· 166 167 t.Run("GetQueued", func(t *testing.T) { 168 results, err := repo.GetQueued(ctx) 169 + shared.AssertNoError(t, err, "Failed to get queued movies") 170 + shared.AssertEqual(t, 2, len(results), "Expected 2 queued movies") 171 172 for _, movie := range results { 173 + shared.AssertEqual(t, "queued", movie.Status, "Expected queued status") 174 } 175 }) 176 177 t.Run("GetWatched", func(t *testing.T) { 178 results, err := repo.GetWatched(ctx) 179 + shared.AssertNoError(t, err, "Failed to get watched movies") 180 + shared.AssertEqual(t, 1, len(results), "Expected 1 watched movie") 181 182 if len(results) > 0 { 183 + shared.AssertEqual(t, "watched", results[0].Status, "Expected watched status") 184 } 185 }) 186 187 t.Run("MarkWatched", func(t *testing.T) { 188 err := repo.MarkWatched(ctx, movie1ID) 189 + shared.AssertNoError(t, err, "Failed to mark movie as watched") 190 191 updated, err := repo.Get(ctx, movie1ID) 192 + shared.AssertNoError(t, err, "Failed to get updated movie") 193 194 + shared.AssertEqual(t, "watched", updated.Status, "Expected status to be watched") 195 + shared.AssertTrue(t, updated.Watched != nil, "Expected watched timestamp to be set") 196 }) 197 }) 198 ··· 209 210 for _, movie := range movies { 211 _, err := repo.Create(ctx, movie) 212 + shared.AssertNoError(t, err, "Failed to create movie") 213 } 214 215 t.Run("Count all movies", func(t *testing.T) { 216 count, err := repo.Count(ctx, MovieListOptions{}) 217 + shared.AssertNoError(t, err, "Failed to count movies") 218 + shared.AssertEqual(t, int64(3), count, "Expected 3 movies") 219 }) 220 221 t.Run("Count queued movies", func(t *testing.T) { 222 count, err := repo.Count(ctx, MovieListOptions{Status: "queued"}) 223 + shared.AssertNoError(t, err, "Failed to count queued movies") 224 + shared.AssertEqual(t, int64(2), count, "Expected 2 queued movies") 225 }) 226 227 t.Run("Count movies by rating", func(t *testing.T) { 228 count, err := repo.Count(ctx, MovieListOptions{MinRating: 8.0}) 229 + shared.AssertNoError(t, err, "Failed to count high-rated movies") 230 + shared.AssertEqual(t, int64(2), count, "Expected 2 movies with rating >= 8.0") 231 }) 232 233 t.Run("Count with context cancellation", func(t *testing.T) { ··· 243 244 movie := NewMovieBuilder().WithTitle("Test Movie").WithYear(2023).Build() 245 id, err := repo.Create(ctx, movie) 246 + shared.AssertNoError(t, err, "Failed to create movie") 247 248 t.Run("Create with cancelled context", func(t *testing.T) { 249 newMovie := NewMovieBuilder().WithTitle("Cancelled").Build() ··· 295 296 t.Run("Get non-existent movie", func(t *testing.T) { 297 _, err := repo.Get(ctx, 99999) 298 + shared.AssertError(t, err, "Expected error for non-existent movie") 299 }) 300 301 t.Run("Update non-existent movie succeeds with no rows affected", func(t *testing.T) { 302 movie := NewMovieBuilder().WithTitle("Non-existent").Build() 303 movie.ID = 99999 304 err := repo.Update(ctx, movie) 305 + shared.AssertNoError(t, err, "Update should not error when no rows affected") 306 }) 307 308 t.Run("Delete non-existent movie succeeds with no rows affected", func(t *testing.T) { 309 err := repo.Delete(ctx, 99999) 310 + shared.AssertNoError(t, err, "Delete should not error when no rows affected") 311 }) 312 313 t.Run("MarkWatched non-existent movie", func(t *testing.T) { 314 err := repo.MarkWatched(ctx, 99999) 315 + shared.AssertError(t, err, "Expected error for non-existent movie") 316 }) 317 318 t.Run("List with no results", func(t *testing.T) { 319 movies, err := repo.List(ctx, MovieListOptions{Year: 1900}) 320 + shared.AssertNoError(t, err, "Should not error when no movies found") 321 + shared.AssertEqual(t, 0, len(movies), "Expected empty result set") 322 }) 323 }) 324 }
+107 -106
internal/repo/note_repository_test.go
··· 6 7 _ "github.com/mattn/go-sqlite3" 8 "github.com/stormlightlabs/noteleaf/internal/models" 9 ) 10 11 func TestNoteRepository(t *testing.T) { ··· 18 note := CreateSampleNote() 19 20 id, err := repo.Create(ctx, note) 21 - AssertNoError(t, err, "Failed to create note") 22 - AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 23 - AssertEqual(t, id, note.ID, "Expected note ID to be set correctly") 24 - AssertFalse(t, note.Created.IsZero(), "Expected Created timestamp to be set") 25 - AssertFalse(t, note.Modified.IsZero(), "Expected Modified timestamp to be set") 26 }) 27 28 t.Run("Get Note", func(t *testing.T) { 29 original := CreateSampleNote() 30 id, err := repo.Create(ctx, original) 31 - AssertNoError(t, err, "Failed to create note") 32 33 retrieved, err := repo.Get(ctx, id) 34 - AssertNoError(t, err, "Failed to get note") 35 36 - AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") 37 - AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") 38 - AssertEqual(t, original.Content, retrieved.Content, "Content mismatch") 39 - AssertEqual(t, len(original.Tags), len(retrieved.Tags), "Tags length mismatch") 40 - AssertEqual(t, original.Archived, retrieved.Archived, "Archived mismatch") 41 - AssertEqual(t, original.FilePath, retrieved.FilePath, "FilePath mismatch") 42 }) 43 44 t.Run("Update Note", func(t *testing.T) { 45 note := CreateSampleNote() 46 id, err := repo.Create(ctx, note) 47 - AssertNoError(t, err, "Failed to create note") 48 49 originalModified := note.Modified 50 ··· 55 note.FilePath = "/new/path/note.md" 56 57 err = repo.Update(ctx, note) 58 - AssertNoError(t, err, "Failed to update note") 59 60 retrieved, err := repo.Get(ctx, id) 61 - AssertNoError(t, err, "Failed to get updated note") 62 63 - AssertEqual(t, "Updated Title", retrieved.Title, "Expected updated title") 64 - AssertEqual(t, "Updated content", retrieved.Content, "Expected updated content") 65 - AssertEqual(t, 2, len(retrieved.Tags), "Expected 2 tags") 66 if len(retrieved.Tags) >= 2 { 67 - AssertEqual(t, "updated", retrieved.Tags[0], "Expected first tag to be 'updated'") 68 - AssertEqual(t, "test", retrieved.Tags[1], "Expected second tag to be 'test'") 69 } 70 - AssertTrue(t, retrieved.Archived, "Expected note to be archived") 71 - AssertEqual(t, "/new/path/note.md", retrieved.FilePath, "Expected updated file path") 72 - AssertTrue(t, retrieved.Modified.After(originalModified), "Expected Modified timestamp to be updated") 73 }) 74 75 t.Run("Delete Note", func(t *testing.T) { 76 note := CreateSampleNote() 77 id, err := repo.Create(ctx, note) 78 - AssertNoError(t, err, "Failed to create note") 79 80 err = repo.Delete(ctx, id) 81 - AssertNoError(t, err, "Failed to delete note") 82 83 _, err = repo.Get(ctx, id) 84 - AssertError(t, err, "Expected error when getting deleted note") 85 }) 86 }) 87 ··· 98 99 for _, note := range notes { 100 _, err := repo.Create(ctx, note) 101 - AssertNoError(t, err, "Failed to create test note") 102 } 103 104 t.Run("List All Notes", func(t *testing.T) { 105 results, err := repo.List(ctx, NoteListOptions{}) 106 - AssertNoError(t, err, "Failed to list notes") 107 - AssertEqual(t, 3, len(results), "Expected 3 notes") 108 }) 109 110 t.Run("List Archived Notes Only", func(t *testing.T) { 111 archived := true 112 results, err := repo.List(ctx, NoteListOptions{Archived: &archived}) 113 - AssertNoError(t, err, "Failed to list archived notes") 114 - AssertEqual(t, 1, len(results), "Expected 1 archived note") 115 if len(results) > 0 { 116 - AssertTrue(t, results[0].Archived, "Retrieved note should be archived") 117 } 118 }) 119 120 t.Run("List Active Notes Only", func(t *testing.T) { 121 archived := false 122 results, err := repo.List(ctx, NoteListOptions{Archived: &archived}) 123 - AssertNoError(t, err, "Failed to list active notes") 124 - AssertEqual(t, 2, len(results), "Expected 2 active notes") 125 for _, note := range results { 126 - AssertFalse(t, note.Archived, "Retrieved note should not be archived") 127 } 128 }) 129 130 t.Run("Search by Title", func(t *testing.T) { 131 results, err := repo.List(ctx, NoteListOptions{Title: "First"}) 132 - AssertNoError(t, err, "Failed to search by title") 133 - AssertEqual(t, 1, len(results), "Expected 1 note") 134 if len(results) > 0 { 135 - AssertEqual(t, "First Note", results[0].Title, "Expected 'First Note'") 136 } 137 }) 138 139 t.Run("Search by Content", func(t *testing.T) { 140 results, err := repo.List(ctx, NoteListOptions{Content: "Important"}) 141 - AssertNoError(t, err, "Failed to search by content") 142 - AssertEqual(t, 1, len(results), "Expected 1 note") 143 if len(results) > 0 { 144 - AssertEqual(t, "Third Note", results[0].Title, "Expected 'Third Note'") 145 } 146 }) 147 148 t.Run("Limit and Offset", func(t *testing.T) { 149 results, err := repo.List(ctx, NoteListOptions{Limit: 2}) 150 - AssertNoError(t, err, "Failed to list with limit") 151 - AssertEqual(t, 2, len(results), "Expected 2 notes") 152 153 results, err = repo.List(ctx, NoteListOptions{Limit: 2, Offset: 1}) 154 - AssertNoError(t, err, "Failed to list with limit and offset") 155 - AssertEqual(t, 2, len(results), "Expected 2 notes with offset") 156 }) 157 }) 158 ··· 169 170 for _, note := range notes { 171 _, err := repo.Create(ctx, note) 172 - AssertNoError(t, err, "Failed to create test note") 173 } 174 175 t.Run("GetByTitle", func(t *testing.T) { 176 results, err := repo.GetByTitle(ctx, "Work") 177 - AssertNoError(t, err, "Failed to get by title") 178 - AssertEqual(t, 1, len(results), "Expected 1 note") 179 if len(results) > 0 { 180 - AssertEqual(t, "Work Note", results[0].Title, "Expected 'Work Note'") 181 } 182 }) 183 184 t.Run("GetArchived", func(t *testing.T) { 185 results, err := repo.GetArchived(ctx) 186 - AssertNoError(t, err, "Failed to get archived notes") 187 - AssertEqual(t, 1, len(results), "Expected 1 archived note") 188 if len(results) > 0 { 189 - AssertTrue(t, results[0].Archived, "Retrieved note should be archived") 190 } 191 }) 192 193 t.Run("GetActive", func(t *testing.T) { 194 results, err := repo.GetActive(ctx) 195 - AssertNoError(t, err, "Failed to get active notes") 196 - AssertEqual(t, 2, len(results), "Expected 2 active notes") 197 for _, note := range results { 198 - AssertFalse(t, note.Archived, "Retrieved note should not be archived") 199 } 200 }) 201 ··· 206 Archived: false, 207 } 208 id, err := repo.Create(ctx, note) 209 - AssertNoError(t, err, "Failed to create note") 210 211 err = repo.Archive(ctx, id) 212 - AssertNoError(t, err, "Failed to archive note") 213 214 retrieved, err := repo.Get(ctx, id) 215 - AssertNoError(t, err, "Failed to get note") 216 - AssertTrue(t, retrieved.Archived, "Note should be archived") 217 218 err = repo.Unarchive(ctx, id) 219 - AssertNoError(t, err, "Failed to unarchive note") 220 221 retrieved, err = repo.Get(ctx, id) 222 - AssertNoError(t, err, "Failed to get note") 223 - AssertFalse(t, retrieved.Archived, "Note should not be archived") 224 }) 225 226 t.Run("SearchContent", func(t *testing.T) { 227 results, err := repo.SearchContent(ctx, "Important") 228 - AssertNoError(t, err, "Failed to search content") 229 - AssertEqual(t, 1, len(results), "Expected 1 note") 230 if len(results) > 0 { 231 - AssertEqual(t, "Important Note", results[0].Title, "Expected 'Important Note'") 232 } 233 }) 234 235 t.Run("GetRecent", func(t *testing.T) { 236 results, err := repo.GetRecent(ctx, 2) 237 - AssertNoError(t, err, "Failed to get recent notes") 238 - AssertEqual(t, 2, len(results), "Expected 2 notes") 239 }) 240 }) 241 ··· 250 Tags: []string{"initial"}, 251 } 252 id, err := repo.Create(ctx, note) 253 - AssertNoError(t, err, "Failed to create note") 254 255 t.Run("AddTag", func(t *testing.T) { 256 err := repo.AddTag(ctx, id, "new-tag") 257 - AssertNoError(t, err, "Failed to add tag") 258 259 retrieved, err := repo.Get(ctx, id) 260 - AssertNoError(t, err, "Failed to get note") 261 262 - AssertEqual(t, 2, len(retrieved.Tags), "Expected 2 tags") 263 264 found := false 265 for _, tag := range retrieved.Tags { ··· 268 break 269 } 270 } 271 - AssertTrue(t, found, "New tag not found in note") 272 }) 273 274 t.Run("AddTag Duplicate", func(t *testing.T) { 275 err := repo.AddTag(ctx, id, "new-tag") 276 - AssertNoError(t, err, "Failed to add duplicate tag") 277 278 retrieved, err := repo.Get(ctx, id) 279 - AssertNoError(t, err, "Failed to get note") 280 281 - AssertEqual(t, 2, len(retrieved.Tags), "Expected 2 tags (no duplicate)") 282 }) 283 284 t.Run("RemoveTag", func(t *testing.T) { 285 err := repo.RemoveTag(ctx, id, "initial") 286 - AssertNoError(t, err, "Failed to remove tag") 287 288 retrieved, err := repo.Get(ctx, id) 289 - AssertNoError(t, err, "Failed to get note") 290 291 - AssertEqual(t, 1, len(retrieved.Tags), "Expected 1 tag after removal") 292 293 for _, tag := range retrieved.Tags { 294 - AssertNotEqual(t, "initial", tag, "Removed tag still found in note") 295 } 296 }) 297 ··· 313 } 314 315 _, err := repo.Create(ctx, note1) 316 - AssertNoError(t, err, "Failed to create note1") 317 _, err = repo.Create(ctx, note2) 318 - AssertNoError(t, err, "Failed to create note2") 319 _, err = repo.Create(ctx, note3) 320 - AssertNoError(t, err, "Failed to create note3") 321 322 results, err := repo.GetByTags(ctx, []string{"work"}) 323 - AssertNoError(t, err, "Failed to get notes by tag") 324 - AssertTrue(t, len(results) >= 2, "Expected at least 2 notes with 'work' tag") 325 326 results, err = repo.GetByTags(ctx, []string{"nonexistent"}) 327 - AssertNoError(t, err, "Failed to get notes by nonexistent tag") 328 - AssertEqual(t, 0, len(results), "Expected 0 notes with nonexistent tag") 329 330 results, err = repo.GetByTags(ctx, []string{}) 331 - AssertNoError(t, err, "Failed to get notes with empty tags") 332 - AssertEqual(t, 0, len(results), "Expected 0 notes with empty tag list") 333 }) 334 }) 335 ··· 340 341 note := NewNoteBuilder().WithTitle("Test Note").WithContent("Test content").Build() 342 id, err := repo.Create(ctx, note) 343 - AssertNoError(t, err, "Failed to create note") 344 345 t.Run("Create with cancelled context", func(t *testing.T) { 346 newNote := NewNoteBuilder().WithTitle("Cancelled").Build() ··· 427 428 t.Run("Get non-existent note", func(t *testing.T) { 429 _, err := repo.Get(ctx, 99999) 430 - AssertError(t, err, "Expected error for non-existent note") 431 }) 432 433 t.Run("Update non-existent note", func(t *testing.T) { ··· 438 } 439 440 err := repo.Update(ctx, note) 441 - AssertError(t, err, "Expected error when updating non-existent note") 442 }) 443 444 t.Run("Delete non-existent note", func(t *testing.T) { 445 err := repo.Delete(ctx, 99999) 446 - AssertError(t, err, "Expected error when deleting non-existent note") 447 }) 448 449 t.Run("Archive non-existent note", func(t *testing.T) { 450 err := repo.Archive(ctx, 99999) 451 - AssertError(t, err, "Expected error when archiving non-existent note") 452 }) 453 454 t.Run("AddTag to non-existent note", func(t *testing.T) { 455 err := repo.AddTag(ctx, 99999, "tag") 456 - AssertError(t, err, "Expected error when adding tag to non-existent note") 457 }) 458 459 t.Run("Note with empty tags", func(t *testing.T) { ··· 464 } 465 466 id, err := repo.Create(ctx, note) 467 - AssertNoError(t, err, "Failed to create note with empty tags") 468 469 retrieved, err := repo.Get(ctx, id) 470 - AssertNoError(t, err, "Failed to get note") 471 472 - AssertEqual(t, 0, len(retrieved.Tags), "Expected empty tags slice") 473 }) 474 475 t.Run("Note with nil tags", func(t *testing.T) { ··· 480 } 481 482 id, err := repo.Create(ctx, note) 483 - AssertNoError(t, err, "Failed to create note with nil tags") 484 485 retrieved, err := repo.Get(ctx, id) 486 - AssertNoError(t, err, "Failed to get note") 487 488 - AssertEqual(t, 0, len(retrieved.Tags), "Expected empty tags") 489 }) 490 491 t.Run("Note with long content", func(t *testing.T) { ··· 500 } 501 502 id, err := repo.Create(ctx, note) 503 - AssertNoError(t, err, "Failed to create note with long content") 504 505 retrieved, err := repo.Get(ctx, id) 506 - AssertNoError(t, err, "Failed to get note") 507 508 - AssertEqual(t, longContent, retrieved.Content, "Long content was not stored/retrieved correctly") 509 }) 510 511 t.Run("List with no results", func(t *testing.T) { 512 notes, err := repo.List(ctx, NoteListOptions{Title: "NonexistentTitle"}) 513 - AssertNoError(t, err, "Should not error when no notes found") 514 - AssertEqual(t, 0, len(notes), "Expected empty result set") 515 }) 516 }) 517 }
··· 6 7 _ "github.com/mattn/go-sqlite3" 8 "github.com/stormlightlabs/noteleaf/internal/models" 9 + "github.com/stormlightlabs/noteleaf/internal/shared" 10 ) 11 12 func TestNoteRepository(t *testing.T) { ··· 19 note := CreateSampleNote() 20 21 id, err := repo.Create(ctx, note) 22 + shared.AssertNoError(t, err, "Failed to create note") 23 + shared.AssertNotEqual(t, int64(0), id, "Expected non-zero ID") 24 + shared.AssertEqual(t, id, note.ID, "Expected note ID to be set correctly") 25 + shared.AssertFalse(t, note.Created.IsZero(), "Expected Created timestamp to be set") 26 + shared.AssertFalse(t, note.Modified.IsZero(), "Expected Modified timestamp to be set") 27 }) 28 29 t.Run("Get Note", func(t *testing.T) { 30 original := CreateSampleNote() 31 id, err := repo.Create(ctx, original) 32 + shared.AssertNoError(t, err, "Failed to create note") 33 34 retrieved, err := repo.Get(ctx, id) 35 + shared.AssertNoError(t, err, "Failed to get note") 36 37 + shared.AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") 38 + shared.AssertEqual(t, original.Title, retrieved.Title, "Title mismatch") 39 + shared.AssertEqual(t, original.Content, retrieved.Content, "Content mismatch") 40 + shared.AssertEqual(t, len(original.Tags), len(retrieved.Tags), "Tags length mismatch") 41 + shared.AssertEqual(t, original.Archived, retrieved.Archived, "Archived mismatch") 42 + shared.AssertEqual(t, original.FilePath, retrieved.FilePath, "FilePath mismatch") 43 }) 44 45 t.Run("Update Note", func(t *testing.T) { 46 note := CreateSampleNote() 47 id, err := repo.Create(ctx, note) 48 + shared.AssertNoError(t, err, "Failed to create note") 49 50 originalModified := note.Modified 51 ··· 56 note.FilePath = "/new/path/note.md" 57 58 err = repo.Update(ctx, note) 59 + shared.AssertNoError(t, err, "Failed to update note") 60 61 retrieved, err := repo.Get(ctx, id) 62 + shared.AssertNoError(t, err, "Failed to get updated note") 63 64 + shared.AssertEqual(t, "Updated Title", retrieved.Title, "Expected updated title") 65 + shared.AssertEqual(t, "Updated content", retrieved.Content, "Expected updated content") 66 + shared.AssertEqual(t, 2, len(retrieved.Tags), "Expected 2 tags") 67 if len(retrieved.Tags) >= 2 { 68 + shared.AssertEqual(t, "updated", retrieved.Tags[0], "Expected first tag to be 'updated'") 69 + shared.AssertEqual(t, "test", retrieved.Tags[1], "Expected second tag to be 'test'") 70 } 71 + shared.AssertTrue(t, retrieved.Archived, "Expected note to be archived") 72 + shared.AssertEqual(t, "/new/path/note.md", retrieved.FilePath, "Expected updated file path") 73 + shared.AssertTrue(t, retrieved.Modified.After(originalModified), "Expected Modified timestamp to be updated") 74 }) 75 76 t.Run("Delete Note", func(t *testing.T) { 77 note := CreateSampleNote() 78 id, err := repo.Create(ctx, note) 79 + shared.AssertNoError(t, err, "Failed to create note") 80 81 err = repo.Delete(ctx, id) 82 + shared.AssertNoError(t, err, "Failed to delete note") 83 84 _, err = repo.Get(ctx, id) 85 + shared.AssertError(t, err, "Expected error when getting deleted note") 86 }) 87 }) 88 ··· 99 100 for _, note := range notes { 101 _, err := repo.Create(ctx, note) 102 + shared.AssertNoError(t, err, "Failed to create test note") 103 } 104 105 t.Run("List All Notes", func(t *testing.T) { 106 results, err := repo.List(ctx, NoteListOptions{}) 107 + shared.AssertNoError(t, err, "Failed to list notes") 108 + shared.AssertEqual(t, 3, len(results), "Expected 3 notes") 109 }) 110 111 t.Run("List Archived Notes Only", func(t *testing.T) { 112 archived := true 113 results, err := repo.List(ctx, NoteListOptions{Archived: &archived}) 114 + shared.AssertNoError(t, err, "Failed to list archived notes") 115 + shared.AssertEqual(t, 1, len(results), "Expected 1 archived note") 116 if len(results) > 0 { 117 + shared.AssertTrue(t, results[0].Archived, "Retrieved note should be archived") 118 } 119 }) 120 121 t.Run("List Active Notes Only", func(t *testing.T) { 122 archived := false 123 results, err := repo.List(ctx, NoteListOptions{Archived: &archived}) 124 + shared.AssertNoError(t, err, "Failed to list active notes") 125 + shared.AssertEqual(t, 2, len(results), "Expected 2 active notes") 126 for _, note := range results { 127 + shared.AssertFalse(t, note.Archived, "Retrieved note should not be archived") 128 } 129 }) 130 131 t.Run("Search by Title", func(t *testing.T) { 132 results, err := repo.List(ctx, NoteListOptions{Title: "First"}) 133 + shared.AssertNoError(t, err, "Failed to search by title") 134 + shared.AssertEqual(t, 1, len(results), "Expected 1 note") 135 if len(results) > 0 { 136 + shared.AssertEqual(t, "First Note", results[0].Title, "Expected 'First Note'") 137 } 138 }) 139 140 t.Run("Search by Content", func(t *testing.T) { 141 results, err := repo.List(ctx, NoteListOptions{Content: "Important"}) 142 + shared.AssertNoError(t, err, "Failed to search by content") 143 + shared.AssertEqual(t, 1, len(results), "Expected 1 note") 144 if len(results) > 0 { 145 + shared.AssertEqual(t, "Third Note", results[0].Title, "Expected 'Third Note'") 146 } 147 }) 148 149 t.Run("Limit and Offset", func(t *testing.T) { 150 results, err := repo.List(ctx, NoteListOptions{Limit: 2}) 151 + shared.AssertNoError(t, err, "Failed to list with limit") 152 + shared.AssertEqual(t, 2, len(results), "Expected 2 notes") 153 154 results, err = repo.List(ctx, NoteListOptions{Limit: 2, Offset: 1}) 155 + shared.AssertNoError(t, err, "Failed to list with limit and offset") 156 + shared.AssertEqual(t, 2, len(results), "Expected 2 notes with offset") 157 }) 158 }) 159 ··· 170 171 for _, note := range notes { 172 _, err := repo.Create(ctx, note) 173 + shared.AssertNoError(t, err, "Failed to create test note") 174 } 175 176 t.Run("GetByTitle", func(t *testing.T) { 177 results, err := repo.GetByTitle(ctx, "Work") 178 + shared.AssertNoError(t, err, "Failed to get by title") 179 + shared.AssertEqual(t, 1, len(results), "Expected 1 note") 180 if len(results) > 0 { 181 + shared.AssertEqual(t, "Work Note", results[0].Title, "Expected 'Work Note'") 182 } 183 }) 184 185 t.Run("GetArchived", func(t *testing.T) { 186 results, err := repo.GetArchived(ctx) 187 + shared.AssertNoError(t, err, "Failed to get archived notes") 188 + shared.AssertEqual(t, 1, len(results), "Expected 1 archived note") 189 if len(results) > 0 { 190 + shared.AssertTrue(t, results[0].Archived, "Retrieved note should be archived") 191 } 192 }) 193 194 t.Run("GetActive", func(t *testing.T) { 195 results, err := repo.GetActive(ctx) 196 + shared.AssertNoError(t, err, "Failed to get active notes") 197 + shared.AssertEqual(t, 2, len(results), "Expected 2 active notes") 198 for _, note := range results { 199 + shared.AssertFalse(t, note.Archived, "Retrieved note should not be archived") 200 } 201 }) 202 ··· 207 Archived: false, 208 } 209 id, err := repo.Create(ctx, note) 210 + shared.AssertNoError(t, err, "Failed to create note") 211 212 err = repo.Archive(ctx, id) 213 + shared.AssertNoError(t, err, "Failed to archive note") 214 215 retrieved, err := repo.Get(ctx, id) 216 + shared.AssertNoError(t, err, "Failed to get note") 217 + shared.AssertTrue(t, retrieved.Archived, "Note should be archived") 218 219 err = repo.Unarchive(ctx, id) 220 + shared.AssertNoError(t, err, "Failed to unarchive note") 221 222 retrieved, err = repo.Get(ctx, id) 223 + shared.AssertNoError(t, err, "Failed to get note") 224 + shared.AssertFalse(t, retrieved.Archived, "Note should not be archived") 225 }) 226 227 t.Run("SearchContent", func(t *testing.T) { 228 results, err := repo.SearchContent(ctx, "Important") 229 + shared.AssertNoError(t, err, "Failed to search content") 230 + shared.AssertEqual(t, 1, len(results), "Expected 1 note") 231 if len(results) > 0 { 232 + shared.AssertEqual(t, "Important Note", results[0].Title, "Expected 'Important Note'") 233 } 234 }) 235 236 t.Run("GetRecent", func(t *testing.T) { 237 results, err := repo.GetRecent(ctx, 2) 238 + shared.AssertNoError(t, err, "Failed to get recent notes") 239 + shared.AssertEqual(t, 2, len(results), "Expected 2 notes") 240 }) 241 }) 242 ··· 251 Tags: []string{"initial"}, 252 } 253 id, err := repo.Create(ctx, note) 254 + shared.AssertNoError(t, err, "Failed to create note") 255 256 t.Run("AddTag", func(t *testing.T) { 257 err := repo.AddTag(ctx, id, "new-tag") 258 + shared.AssertNoError(t, err, "Failed to add tag") 259 260 retrieved, err := repo.Get(ctx, id) 261 + shared.AssertNoError(t, err, "Failed to get note") 262 263 + shared.AssertEqual(t, 2, len(retrieved.Tags), "Expected 2 tags") 264 265 found := false 266 for _, tag := range retrieved.Tags { ··· 269 break 270 } 271 } 272 + shared.AssertTrue(t, found, "New tag not found in note") 273 }) 274 275 t.Run("AddTag Duplicate", func(t *testing.T) { 276 err := repo.AddTag(ctx, id, "new-tag") 277 + shared.AssertNoError(t, err, "Failed to add duplicate tag") 278 279 retrieved, err := repo.Get(ctx, id) 280 + shared.AssertNoError(t, err, "Failed to get note") 281 282 + shared.AssertEqual(t, 2, len(retrieved.Tags), "Expected 2 tags (no duplicate)") 283 }) 284 285 t.Run("RemoveTag", func(t *testing.T) { 286 err := repo.RemoveTag(ctx, id, "initial") 287 + shared.AssertNoError(t, err, "Failed to remove tag") 288 289 retrieved, err := repo.Get(ctx, id) 290 + shared.AssertNoError(t, err, "Failed to get note") 291 292 + shared.AssertEqual(t, 1, len(retrieved.Tags), "Expected 1 tag after removal") 293 294 for _, tag := range retrieved.Tags { 295 + shared.AssertNotEqual(t, "initial", tag, "Removed tag still found in note") 296 } 297 }) 298 ··· 314 } 315 316 _, err := repo.Create(ctx, note1) 317 + shared.AssertNoError(t, err, "Failed to create note1") 318 _, err = repo.Create(ctx, note2) 319 + shared.AssertNoError(t, err, "Failed to create note2") 320 _, err = repo.Create(ctx, note3) 321 + shared.AssertNoError(t, err, "Failed to create note3") 322 323 results, err := repo.GetByTags(ctx, []string{"work"}) 324 + shared.AssertNoError(t, err, "Failed to get notes by tag") 325 + shared.AssertTrue(t, len(results) >= 2, "Expected at least 2 notes with 'work' tag") 326 327 results, err = repo.GetByTags(ctx, []string{"nonexistent"}) 328 + shared.AssertNoError(t, err, "Failed to get notes by nonexistent tag") 329 + shared.AssertEqual(t, 0, len(results), "Expected 0 notes with nonexistent tag") 330 331 results, err = repo.GetByTags(ctx, []string{}) 332 + shared.AssertNoError(t, err, "Failed to get notes with empty tags") 333 + shared.AssertEqual(t, 0, len(results), "Expected 0 notes with empty tag list") 334 }) 335 }) 336 ··· 341 342 note := NewNoteBuilder().WithTitle("Test Note").WithContent("Test content").Build() 343 id, err := repo.Create(ctx, note) 344 + shared.AssertNoError(t, err, "Failed to create note") 345 346 t.Run("Create with cancelled context", func(t *testing.T) { 347 newNote := NewNoteBuilder().WithTitle("Cancelled").Build() ··· 428 429 t.Run("Get non-existent note", func(t *testing.T) { 430 _, err := repo.Get(ctx, 99999) 431 + shared.AssertError(t, err, "Expected error for non-existent note") 432 }) 433 434 t.Run("Update non-existent note", func(t *testing.T) { ··· 439 } 440 441 err := repo.Update(ctx, note) 442 + shared.AssertError(t, err, "Expected error when updating non-existent note") 443 }) 444 445 t.Run("Delete non-existent note", func(t *testing.T) { 446 err := repo.Delete(ctx, 99999) 447 + shared.AssertError(t, err, "Expected error when deleting non-existent note") 448 }) 449 450 t.Run("Archive non-existent note", func(t *testing.T) { 451 err := repo.Archive(ctx, 99999) 452 + shared.AssertError(t, err, "Expected error when archiving non-existent note") 453 }) 454 455 t.Run("AddTag to non-existent note", func(t *testing.T) { 456 err := repo.AddTag(ctx, 99999, "tag") 457 + shared.AssertError(t, err, "Expected error when adding tag to non-existent note") 458 }) 459 460 t.Run("Note with empty tags", func(t *testing.T) { ··· 465 } 466 467 id, err := repo.Create(ctx, note) 468 + shared.AssertNoError(t, err, "Failed to create note with empty tags") 469 470 retrieved, err := repo.Get(ctx, id) 471 + shared.AssertNoError(t, err, "Failed to get note") 472 473 + shared.AssertEqual(t, 0, len(retrieved.Tags), "Expected empty tags slice") 474 }) 475 476 t.Run("Note with nil tags", func(t *testing.T) { ··· 481 } 482 483 id, err := repo.Create(ctx, note) 484 + shared.AssertNoError(t, err, "Failed to create note with nil tags") 485 486 retrieved, err := repo.Get(ctx, id) 487 + shared.AssertNoError(t, err, "Failed to get note") 488 489 + shared.AssertEqual(t, 0, len(retrieved.Tags), "Expected empty tags") 490 }) 491 492 t.Run("Note with long content", func(t *testing.T) { ··· 501 } 502 503 id, err := repo.Create(ctx, note) 504 + shared.AssertNoError(t, err, "Failed to create note with long content") 505 506 retrieved, err := repo.Get(ctx, id) 507 + shared.AssertNoError(t, err, "Failed to get note") 508 509 + shared.AssertEqual(t, longContent, retrieved.Content, "Long content was not stored/retrieved correctly") 510 }) 511 512 t.Run("List with no results", func(t *testing.T) { 513 notes, err := repo.List(ctx, NoteListOptions{Title: "NonexistentTitle"}) 514 + shared.AssertNoError(t, err, "Should not error when no notes found") 515 + shared.AssertEqual(t, 0, len(notes), "Expected empty result set") 516 }) 517 }) 518 }
+36 -35
internal/repo/task_repository_test.go
··· 10 "github.com/google/uuid" 11 _ "github.com/mattn/go-sqlite3" 12 "github.com/stormlightlabs/noteleaf/internal/models" 13 ) 14 15 func newUUID() string { ··· 156 t.Run("when called with context cancellation", func(t *testing.T) { 157 task := CreateSampleTask() 158 _, err := repo.Create(ctx, task) 159 - AssertNoError(t, err, "Failed to create task") 160 161 task.Description = "Updated" 162 err = repo.Update(NewCanceledContext(), task) ··· 923 defer func() { marshalTaskTags = orig }() 924 925 _, err := repo.Create(ctx, CreateSampleTask()) 926 - AssertError(t, err, "expected MarshalTags error") 927 - AssertContains(t, err.Error(), "failed to marshal tags", "error message") 928 }) 929 930 t.Run("Create fails on MarshalAnnotations error", func(t *testing.T) { ··· 935 defer func() { marshalTaskAnnotations = orig }() 936 937 _, err := repo.Create(ctx, CreateSampleTask()) 938 - AssertError(t, err, "expected MarshalAnnotations error") 939 - AssertContains(t, err.Error(), "failed to marshal annotations", "error message") 940 }) 941 942 t.Run("Update fails on MarshalTags error", func(t *testing.T) { 943 task := CreateSampleTask() 944 id, err := repo.Create(ctx, task) 945 - AssertNoError(t, err, "create should succeed") 946 947 orig := marshalTaskTags 948 marshalTaskTags = func(t *models.Task) (string, error) { ··· 952 953 task.ID = id 954 err = repo.Update(ctx, task) 955 - AssertError(t, err, "expected MarshalTags error") 956 - AssertContains(t, err.Error(), "failed to marshal tags", "error message") 957 }) 958 959 t.Run("Update fails on MarshalAnnotations error", func(t *testing.T) { 960 task := CreateSampleTask() 961 id, err := repo.Create(ctx, task) 962 - AssertNoError(t, err, "create should succeed") 963 964 orig := marshalTaskAnnotations 965 marshalTaskAnnotations = func(t *models.Task) (string, error) { ··· 969 970 task.ID = id 971 err = repo.Update(ctx, task) 972 - AssertError(t, err, "expected MarshalAnnotations error") 973 - AssertContains(t, err.Error(), "failed to marshal annotations", "error message") 974 }) 975 976 t.Run("Get fails on UnmarshalTags error", func(t *testing.T) { 977 task := CreateSampleTask() 978 task.Tags = []string{"test"} 979 id, err := repo.Create(ctx, task) 980 - AssertNoError(t, err, "create should succeed") 981 982 orig := unmarshalTaskTags 983 unmarshalTaskTags = func(t *models.Task, s string) error { ··· 986 defer func() { unmarshalTaskTags = orig }() 987 988 _, err = repo.Get(ctx, id) 989 - AssertError(t, err, "expected UnmarshalTags error") 990 - AssertContains(t, err.Error(), "failed to unmarshal tags", "error message") 991 }) 992 993 t.Run("Get fails on UnmarshalAnnotations error", func(t *testing.T) { 994 task := CreateSampleTask() 995 task.Annotations = []string{"test"} 996 id, err := repo.Create(ctx, task) 997 - AssertNoError(t, err, "create should succeed") 998 999 orig := unmarshalTaskAnnotations 1000 unmarshalTaskAnnotations = func(t *models.Task, s string) error { ··· 1003 defer func() { unmarshalTaskAnnotations = orig }() 1004 1005 _, err = repo.Get(ctx, id) 1006 - AssertError(t, err, "expected UnmarshalAnnotations error") 1007 - AssertContains(t, err.Error(), "failed to unmarshal annotations", "error message") 1008 }) 1009 1010 t.Run("GetByUUID fails on UnmarshalTags error", func(t *testing.T) { 1011 task := CreateSampleTask() 1012 task.Tags = []string{"test"} 1013 _, err := repo.Create(ctx, task) 1014 - AssertNoError(t, err, "create should succeed") 1015 1016 orig := unmarshalTaskTags 1017 unmarshalTaskTags = func(t *models.Task, s string) error { ··· 1020 defer func() { unmarshalTaskTags = orig }() 1021 1022 _, err = repo.GetByUUID(ctx, task.UUID) 1023 - AssertError(t, err, "expected UnmarshalTags error") 1024 - AssertContains(t, err.Error(), "failed to unmarshal tags", "error message") 1025 }) 1026 1027 t.Run("GetByUUID fails on UnmarshalAnnotations error", func(t *testing.T) { 1028 task := CreateSampleTask() 1029 task.Annotations = []string{"test"} 1030 _, err := repo.Create(ctx, task) 1031 - AssertNoError(t, err, "create should succeed") 1032 1033 orig := unmarshalTaskAnnotations 1034 unmarshalTaskAnnotations = func(t *models.Task, s string) error { ··· 1037 defer func() { unmarshalTaskAnnotations = orig }() 1038 1039 _, err = repo.GetByUUID(ctx, task.UUID) 1040 - AssertError(t, err, "expected UnmarshalAnnotations error") 1041 - AssertContains(t, err.Error(), "failed to unmarshal annotations", "error message") 1042 }) 1043 }) 1044 ··· 1101 t.Run("GetByContext", func(t *testing.T) { 1102 task1 := NewTaskBuilder().WithContext("work").WithDescription("Work task 1").Build() 1103 _, err := repo.Create(ctx, task1) 1104 - AssertNoError(t, err, "Failed to create task1") 1105 1106 task2 := NewTaskBuilder().WithContext("home").WithDescription("Home task 1").Build() 1107 _, err = repo.Create(ctx, task2) 1108 - AssertNoError(t, err, "Failed to create task2") 1109 1110 task3 := NewTaskBuilder().WithContext("work").WithDescription("Work task 2").Build() 1111 _, err = repo.Create(ctx, task3) 1112 - AssertNoError(t, err, "Failed to create task3") 1113 1114 workTasks, err := repo.GetByContext(ctx, "work") 1115 if err != nil { ··· 1142 blocker := CreateSampleTask() 1143 blocker.Description = "Blocker task" 1144 _, err := repo.Create(ctx, blocker) 1145 - AssertNoError(t, err, "create blocker should succeed") 1146 1147 blocked1 := CreateSampleTask() 1148 blocked1.Description = "Blocked task 1" 1149 blocked1.DependsOn = []string{blocker.UUID} 1150 _, err = repo.Create(ctx, blocked1) 1151 - AssertNoError(t, err, "create blocked1 should succeed") 1152 1153 blocked2 := CreateSampleTask() 1154 blocked2.Description = "Blocked task 2" 1155 blocked2.DependsOn = []string{blocker.UUID} 1156 _, err = repo.Create(ctx, blocked2) 1157 - AssertNoError(t, err, "create blocked2 should succeed") 1158 1159 independent := CreateSampleTask() 1160 independent.Description = "Independent task" 1161 _, err = repo.Create(ctx, independent) 1162 - AssertNoError(t, err, "create independent should succeed") 1163 1164 blockedTasks, err := repo.GetBlockedTasks(ctx, blocker.UUID) 1165 - AssertNoError(t, err, "GetBlockedTasks should succeed") 1166 - AssertEqual(t, 2, len(blockedTasks), "should find 2 blocked tasks") 1167 1168 for _, task := range blockedTasks { 1169 - AssertTrue(t, slices.Contains(task.DependsOn, blocker.UUID), "task should depend on blocker") 1170 } 1171 1172 emptyBlocked, err := repo.GetBlockedTasks(ctx, independent.UUID) 1173 - AssertNoError(t, err, "GetBlockedTasks for independent should succeed") 1174 - AssertEqual(t, 0, len(emptyBlocked), "independent task should not block anything") 1175 }) 1176 }
··· 10 "github.com/google/uuid" 11 _ "github.com/mattn/go-sqlite3" 12 "github.com/stormlightlabs/noteleaf/internal/models" 13 + "github.com/stormlightlabs/noteleaf/internal/shared" 14 ) 15 16 func newUUID() string { ··· 157 t.Run("when called with context cancellation", func(t *testing.T) { 158 task := CreateSampleTask() 159 _, err := repo.Create(ctx, task) 160 + shared.AssertNoError(t, err, "Failed to create task") 161 162 task.Description = "Updated" 163 err = repo.Update(NewCanceledContext(), task) ··· 924 defer func() { marshalTaskTags = orig }() 925 926 _, err := repo.Create(ctx, CreateSampleTask()) 927 + shared.AssertError(t, err, "expected MarshalTags error") 928 + shared.AssertContains(t, err.Error(), "failed to marshal tags", "error message") 929 }) 930 931 t.Run("Create fails on MarshalAnnotations error", func(t *testing.T) { ··· 936 defer func() { marshalTaskAnnotations = orig }() 937 938 _, err := repo.Create(ctx, CreateSampleTask()) 939 + shared.AssertError(t, err, "expected MarshalAnnotations error") 940 + shared.AssertContains(t, err.Error(), "failed to marshal annotations", "error message") 941 }) 942 943 t.Run("Update fails on MarshalTags error", func(t *testing.T) { 944 task := CreateSampleTask() 945 id, err := repo.Create(ctx, task) 946 + shared.AssertNoError(t, err, "create should succeed") 947 948 orig := marshalTaskTags 949 marshalTaskTags = func(t *models.Task) (string, error) { ··· 953 954 task.ID = id 955 err = repo.Update(ctx, task) 956 + shared.AssertError(t, err, "expected MarshalTags error") 957 + shared.AssertContains(t, err.Error(), "failed to marshal tags", "error message") 958 }) 959 960 t.Run("Update fails on MarshalAnnotations error", func(t *testing.T) { 961 task := CreateSampleTask() 962 id, err := repo.Create(ctx, task) 963 + shared.AssertNoError(t, err, "create should succeed") 964 965 orig := marshalTaskAnnotations 966 marshalTaskAnnotations = func(t *models.Task) (string, error) { ··· 970 971 task.ID = id 972 err = repo.Update(ctx, task) 973 + shared.AssertError(t, err, "expected MarshalAnnotations error") 974 + shared.AssertContains(t, err.Error(), "failed to marshal annotations", "error message") 975 }) 976 977 t.Run("Get fails on UnmarshalTags error", func(t *testing.T) { 978 task := CreateSampleTask() 979 task.Tags = []string{"test"} 980 id, err := repo.Create(ctx, task) 981 + shared.AssertNoError(t, err, "create should succeed") 982 983 orig := unmarshalTaskTags 984 unmarshalTaskTags = func(t *models.Task, s string) error { ··· 987 defer func() { unmarshalTaskTags = orig }() 988 989 _, err = repo.Get(ctx, id) 990 + shared.AssertError(t, err, "expected UnmarshalTags error") 991 + shared.AssertContains(t, err.Error(), "failed to unmarshal tags", "error message") 992 }) 993 994 t.Run("Get fails on UnmarshalAnnotations error", func(t *testing.T) { 995 task := CreateSampleTask() 996 task.Annotations = []string{"test"} 997 id, err := repo.Create(ctx, task) 998 + shared.AssertNoError(t, err, "create should succeed") 999 1000 orig := unmarshalTaskAnnotations 1001 unmarshalTaskAnnotations = func(t *models.Task, s string) error { ··· 1004 defer func() { unmarshalTaskAnnotations = orig }() 1005 1006 _, err = repo.Get(ctx, id) 1007 + shared.AssertError(t, err, "expected UnmarshalAnnotations error") 1008 + shared.AssertContains(t, err.Error(), "failed to unmarshal annotations", "error message") 1009 }) 1010 1011 t.Run("GetByUUID fails on UnmarshalTags error", func(t *testing.T) { 1012 task := CreateSampleTask() 1013 task.Tags = []string{"test"} 1014 _, err := repo.Create(ctx, task) 1015 + shared.AssertNoError(t, err, "create should succeed") 1016 1017 orig := unmarshalTaskTags 1018 unmarshalTaskTags = func(t *models.Task, s string) error { ··· 1021 defer func() { unmarshalTaskTags = orig }() 1022 1023 _, err = repo.GetByUUID(ctx, task.UUID) 1024 + shared.AssertError(t, err, "expected UnmarshalTags error") 1025 + shared.AssertContains(t, err.Error(), "failed to unmarshal tags", "error message") 1026 }) 1027 1028 t.Run("GetByUUID fails on UnmarshalAnnotations error", func(t *testing.T) { 1029 task := CreateSampleTask() 1030 task.Annotations = []string{"test"} 1031 _, err := repo.Create(ctx, task) 1032 + shared.AssertNoError(t, err, "create should succeed") 1033 1034 orig := unmarshalTaskAnnotations 1035 unmarshalTaskAnnotations = func(t *models.Task, s string) error { ··· 1038 defer func() { unmarshalTaskAnnotations = orig }() 1039 1040 _, err = repo.GetByUUID(ctx, task.UUID) 1041 + shared.AssertError(t, err, "expected UnmarshalAnnotations error") 1042 + shared.AssertContains(t, err.Error(), "failed to unmarshal annotations", "error message") 1043 }) 1044 }) 1045 ··· 1102 t.Run("GetByContext", func(t *testing.T) { 1103 task1 := NewTaskBuilder().WithContext("work").WithDescription("Work task 1").Build() 1104 _, err := repo.Create(ctx, task1) 1105 + shared.AssertNoError(t, err, "Failed to create task1") 1106 1107 task2 := NewTaskBuilder().WithContext("home").WithDescription("Home task 1").Build() 1108 _, err = repo.Create(ctx, task2) 1109 + shared.AssertNoError(t, err, "Failed to create task2") 1110 1111 task3 := NewTaskBuilder().WithContext("work").WithDescription("Work task 2").Build() 1112 _, err = repo.Create(ctx, task3) 1113 + shared.AssertNoError(t, err, "Failed to create task3") 1114 1115 workTasks, err := repo.GetByContext(ctx, "work") 1116 if err != nil { ··· 1143 blocker := CreateSampleTask() 1144 blocker.Description = "Blocker task" 1145 _, err := repo.Create(ctx, blocker) 1146 + shared.AssertNoError(t, err, "create blocker should succeed") 1147 1148 blocked1 := CreateSampleTask() 1149 blocked1.Description = "Blocked task 1" 1150 blocked1.DependsOn = []string{blocker.UUID} 1151 _, err = repo.Create(ctx, blocked1) 1152 + shared.AssertNoError(t, err, "create blocked1 should succeed") 1153 1154 blocked2 := CreateSampleTask() 1155 blocked2.Description = "Blocked task 2" 1156 blocked2.DependsOn = []string{blocker.UUID} 1157 _, err = repo.Create(ctx, blocked2) 1158 + shared.AssertNoError(t, err, "create blocked2 should succeed") 1159 1160 independent := CreateSampleTask() 1161 independent.Description = "Independent task" 1162 _, err = repo.Create(ctx, independent) 1163 + shared.AssertNoError(t, err, "create independent should succeed") 1164 1165 blockedTasks, err := repo.GetBlockedTasks(ctx, blocker.UUID) 1166 + shared.AssertNoError(t, err, "GetBlockedTasks should succeed") 1167 + shared.AssertEqual(t, 2, len(blockedTasks), "should find 2 blocked tasks") 1168 1169 for _, task := range blockedTasks { 1170 + shared.AssertTrue(t, slices.Contains(task.DependsOn, blocker.UUID), "task should depend on blocker") 1171 } 1172 1173 emptyBlocked, err := repo.GetBlockedTasks(ctx, independent.UUID) 1174 + shared.AssertNoError(t, err, "GetBlockedTasks for independent should succeed") 1175 + shared.AssertEqual(t, 0, len(emptyBlocked), "independent task should not block anything") 1176 }) 1177 }
+12 -88
internal/repo/test_utilities.go
··· 12 "github.com/jaswdr/faker/v2" 13 _ "github.com/mattn/go-sqlite3" 14 "github.com/stormlightlabs/noteleaf/internal/models" 15 "github.com/stormlightlabs/noteleaf/internal/store" 16 ) 17 ··· 169 return articles 170 } 171 172 - func AssertNoError(t *testing.T, err error, msg string) { 173 - t.Helper() 174 - if err != nil { 175 - t.Fatalf("%s: %v", msg, err) 176 - } 177 - } 178 - 179 - func AssertError(t *testing.T, err error, msg string) { 180 - t.Helper() 181 - if err == nil { 182 - t.Fatalf("%s: expected error but got none", msg) 183 - } 184 - } 185 - 186 func AssertCancelledContext(t *testing.T, err error) { 187 - AssertError(t, err, "Expected error with cancelled context") 188 - } 189 - 190 - func AssertEqual[T comparable](t *testing.T, expected, actual T, msg string) { 191 - t.Helper() 192 - if expected != actual { 193 - t.Fatalf("%s: expected %v, got %v", msg, expected, actual) 194 - } 195 - } 196 - 197 - func AssertNotEqual[T comparable](t *testing.T, notExpected, actual T, msg string) { 198 - t.Helper() 199 - if notExpected == actual { 200 - t.Fatalf("%s: expected value to not equal %v", msg, notExpected) 201 - } 202 - } 203 - 204 - func AssertTrue(t *testing.T, condition bool, msg string) { 205 - t.Helper() 206 - if !condition { 207 - t.Fatalf("%s: expected true", msg) 208 - } 209 - } 210 - 211 - func AssertFalse(t *testing.T, condition bool, msg string) { 212 - t.Helper() 213 - if condition { 214 - t.Fatalf("%s: expected false", msg) 215 - } 216 - } 217 - 218 - func AssertContains(t *testing.T, str, substr, msg string) { 219 - t.Helper() 220 - if !strings.Contains(str, substr) { 221 - t.Fatalf("%s: expected string '%s' to contain '%s'", msg, str, substr) 222 - } 223 - } 224 - 225 - func AssertNil(t *testing.T, value any, msg string) { 226 - t.Helper() 227 - if value != nil { 228 - t.Fatalf("%s: expected nil, got %v", msg, value) 229 - } 230 - } 231 - 232 - func AssertNotNil(t *testing.T, value any, msg string) { 233 - t.Helper() 234 - if value == nil { 235 - t.Fatalf("%s: expected non-nil value", msg) 236 - } 237 - } 238 - 239 - func AssertGreaterThan[T interface{ int | int64 | float64 }](t *testing.T, actual, threshold T, msg string) { 240 - t.Helper() 241 - if actual <= threshold { 242 - t.Fatalf("%s: expected %v > %v", msg, actual, threshold) 243 - } 244 - } 245 - 246 - func AssertLessThan[T interface{ int | int64 | float64 }](t *testing.T, actual, threshold T, msg string) { 247 - t.Helper() 248 - if actual >= threshold { 249 - t.Fatalf("%s: expected %v < %v", msg, actual, threshold) 250 - } 251 } 252 253 // NewCanceledContext returns a pre-canceled context for testing error conditions ··· 565 task2.Priority = "low" 566 567 id1, err := repos.Tasks.Create(ctx, task1) 568 - AssertNoError(t, err, "Failed to create sample task 1") 569 task1.ID = id1 570 571 id2, err := repos.Tasks.Create(ctx, task2) 572 - AssertNoError(t, err, "Failed to create sample task 2") 573 task2.ID = id2 574 575 book1 := CreateSampleBook() ··· 581 book2.Status = "finished" 582 583 bookID1, err := repos.Books.Create(ctx, book1) 584 - AssertNoError(t, err, "Failed to create sample book 1") 585 book1.ID = bookID1 586 587 bookID2, err := repos.Books.Create(ctx, book2) 588 - AssertNoError(t, err, "Failed to create sample book 2") 589 book2.ID = bookID2 590 591 movie1 := CreateSampleMovie() ··· 597 movie2.Status = "watched" 598 599 movieID1, err := repos.Movies.Create(ctx, movie1) 600 - AssertNoError(t, err, "Failed to create sample movie 1") 601 movie1.ID = movieID1 602 603 movieID2, err := repos.Movies.Create(ctx, movie2) 604 - AssertNoError(t, err, "Failed to create sample movie 2") 605 movie2.ID = movieID2 606 607 tv1 := CreateSampleTVShow() ··· 613 tv2.Status = "watching" 614 615 tvID1, err := repos.TV.Create(ctx, tv1) 616 - AssertNoError(t, err, "Failed to create sample TV show 1") 617 tv1.ID = tvID1 618 619 tvID2, err := repos.TV.Create(ctx, tv2) 620 - AssertNoError(t, err, "Failed to create sample TV show 2") 621 tv2.ID = tvID2 622 623 note1 := CreateSampleNote() ··· 630 note2.Archived = true 631 632 noteID1, err := repos.Notes.Create(ctx, note1) 633 - AssertNoError(t, err, "Failed to create sample note 1") 634 note1.ID = noteID1 635 636 noteID2, err := repos.Notes.Create(ctx, note2) 637 - AssertNoError(t, err, "Failed to create sample note 2") 638 note2.ID = noteID2 639 640 return repos
··· 12 "github.com/jaswdr/faker/v2" 13 _ "github.com/mattn/go-sqlite3" 14 "github.com/stormlightlabs/noteleaf/internal/models" 15 + "github.com/stormlightlabs/noteleaf/internal/shared" 16 "github.com/stormlightlabs/noteleaf/internal/store" 17 ) 18 ··· 170 return articles 171 } 172 173 func AssertCancelledContext(t *testing.T, err error) { 174 + shared.AssertError(t, err, "Expected error with cancelled context") 175 } 176 177 // NewCanceledContext returns a pre-canceled context for testing error conditions ··· 489 task2.Priority = "low" 490 491 id1, err := repos.Tasks.Create(ctx, task1) 492 + shared.AssertNoError(t, err, "Failed to create sample task 1") 493 task1.ID = id1 494 495 id2, err := repos.Tasks.Create(ctx, task2) 496 + shared.AssertNoError(t, err, "Failed to create sample task 2") 497 task2.ID = id2 498 499 book1 := CreateSampleBook() ··· 505 book2.Status = "finished" 506 507 bookID1, err := repos.Books.Create(ctx, book1) 508 + shared.AssertNoError(t, err, "Failed to create sample book 1") 509 book1.ID = bookID1 510 511 bookID2, err := repos.Books.Create(ctx, book2) 512 + shared.AssertNoError(t, err, "Failed to create sample book 2") 513 book2.ID = bookID2 514 515 movie1 := CreateSampleMovie() ··· 521 movie2.Status = "watched" 522 523 movieID1, err := repos.Movies.Create(ctx, movie1) 524 + shared.AssertNoError(t, err, "Failed to create sample movie 1") 525 movie1.ID = movieID1 526 527 movieID2, err := repos.Movies.Create(ctx, movie2) 528 + shared.AssertNoError(t, err, "Failed to create sample movie 2") 529 movie2.ID = movieID2 530 531 tv1 := CreateSampleTVShow() ··· 537 tv2.Status = "watching" 538 539 tvID1, err := repos.TV.Create(ctx, tv1) 540 + shared.AssertNoError(t, err, "Failed to create sample TV show 1") 541 tv1.ID = tvID1 542 543 tvID2, err := repos.TV.Create(ctx, tv2) 544 + shared.AssertNoError(t, err, "Failed to create sample TV show 2") 545 tv2.ID = tvID2 546 547 note1 := CreateSampleNote() ··· 554 note2.Archived = true 555 556 noteID1, err := repos.Notes.Create(ctx, note1) 557 + shared.AssertNoError(t, err, "Failed to create sample note 1") 558 note1.ID = noteID1 559 560 noteID2, err := repos.Notes.Create(ctx, note2) 561 + shared.AssertNoError(t, err, "Failed to create sample note 2") 562 note2.ID = noteID2 563 564 return repos
+79 -78
internal/repo/time_entry_repository_test.go
··· 9 10 _ "github.com/mattn/go-sqlite3" 11 "github.com/stormlightlabs/noteleaf/internal/models" 12 ) 13 14 func createTestTask(t *testing.T, db *sql.DB) *models.Task { ··· 22 } 23 24 id, err := taskRepo.Create(ctx, task) 25 - AssertNoError(t, err, "Failed to create test task") 26 task.ID = id 27 return task 28 } ··· 38 description := "Working on feature" 39 entry, err := repo.Start(ctx, task.ID, description) 40 41 - AssertNoError(t, err, "Failed to start time tracking") 42 - AssertNotEqual(t, int64(0), entry.ID, "Expected non-zero entry ID") 43 - AssertEqual(t, task.ID, entry.TaskID, "Expected TaskID to match") 44 - AssertEqual(t, description, entry.Description, "Expected description to match") 45 - AssertTrue(t, entry.EndTime == nil, "Expected EndTime to be nil for active entry") 46 - AssertTrue(t, entry.IsActive(), "Expected entry to be active") 47 }) 48 49 t.Run("Prevent starting already active task", func(t *testing.T) { 50 _, err := repo.Start(ctx, task.ID, "Another attempt") 51 52 - AssertError(t, err, "Expected error when starting already active task") 53 - AssertContains(t, err.Error(), "task already has an active time entry", "Expected specific error message") 54 }) 55 56 t.Run("Stop active time entry", func(t *testing.T) { ··· 59 task := createTestTask(t, db) 60 61 entry, err := repo.Start(ctx, task.ID, "Test work") 62 - AssertNoError(t, err, "Failed to start time tracking") 63 64 time.Sleep(1010 * time.Millisecond) 65 66 stoppedEntry, err := repo.Stop(ctx, entry.ID) 67 - AssertNoError(t, err, "Failed to stop time tracking") 68 - AssertTrue(t, stoppedEntry.EndTime != nil, "Expected EndTime to be set") 69 - AssertGreaterThan(t, stoppedEntry.DurationSeconds, int64(0), "Expected duration > 0") 70 - AssertFalse(t, stoppedEntry.IsActive(), "Expected entry to not be active after stopping") 71 }) 72 73 t.Run("Fail to stop already stopped entry", func(t *testing.T) { ··· 76 task := createTestTask(t, db) 77 78 entry, err := repo.Start(ctx, task.ID, "Test work") 79 - AssertNoError(t, err, "Failed to start time tracking") 80 81 time.Sleep(1010 * time.Millisecond) 82 _, err = repo.Stop(ctx, entry.ID) 83 - AssertNoError(t, err, "Failed to stop time tracking") 84 85 _, err = repo.Stop(ctx, entry.ID) 86 - AssertError(t, err, "Expected error when stopping already stopped entry") 87 - AssertContains(t, err.Error(), "time entry is not active", "Expected specific error message") 88 }) 89 90 t.Run("Get time entry", func(t *testing.T) { ··· 93 task := createTestTask(t, db) 94 95 original, err := repo.Start(ctx, task.ID, "Test entry") 96 - AssertNoError(t, err, "Failed to start time tracking") 97 98 retrieved, err := repo.Get(ctx, original.ID) 99 - AssertNoError(t, err, "Failed to get time entry") 100 - AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") 101 - AssertEqual(t, original.TaskID, retrieved.TaskID, "TaskID mismatch") 102 - AssertEqual(t, original.Description, retrieved.Description, "Description mismatch") 103 }) 104 105 t.Run("Delete time entry", func(t *testing.T) { ··· 108 task := createTestTask(t, db) 109 110 entry, err := repo.Start(ctx, task.ID, "To be deleted") 111 - AssertNoError(t, err, "Failed to create entry") 112 113 err = repo.Delete(ctx, entry.ID) 114 - AssertNoError(t, err, "Failed to delete entry") 115 116 _, err = repo.Get(ctx, entry.ID) 117 - AssertError(t, err, "Expected error when getting deleted entry") 118 - AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") 119 }) 120 }) 121 ··· 127 128 t.Run("GetActiveByTaskID returns error when no active entry", func(t *testing.T) { 129 _, err := repo.GetActiveByTaskID(ctx, task.ID) 130 - AssertError(t, err, "Expected error when no active entry exists") 131 - AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") 132 }) 133 134 t.Run("GetActiveByTaskID returns active entry", func(t *testing.T) { 135 startedEntry, err := repo.Start(ctx, task.ID, "Test work") 136 - AssertNoError(t, err, "Failed to start time tracking") 137 138 activeEntry, err := repo.GetActiveByTaskID(ctx, task.ID) 139 - AssertNoError(t, err, "Failed to get active entry") 140 - AssertEqual(t, startedEntry.ID, activeEntry.ID, "Expected entry IDs to match") 141 - AssertTrue(t, activeEntry.IsActive(), "Expected entry to be active") 142 }) 143 144 t.Run("StopActiveByTaskID stops active entry", func(t *testing.T) { ··· 147 task := createTestTask(t, db) 148 149 _, err := repo.Start(ctx, task.ID, "Test work") 150 - AssertNoError(t, err, "Failed to start time tracking") 151 152 stoppedEntry, err := repo.StopActiveByTaskID(ctx, task.ID) 153 - AssertNoError(t, err, "Failed to stop time tracking by task ID") 154 - AssertTrue(t, stoppedEntry.EndTime != nil, "Expected EndTime to be set") 155 - AssertFalse(t, stoppedEntry.IsActive(), "Expected entry to not be active") 156 }) 157 158 t.Run("StopActiveByTaskID fails when no active entry", func(t *testing.T) { ··· 161 task := createTestTask(t, db) 162 163 _, err := repo.StopActiveByTaskID(ctx, task.ID) 164 - AssertError(t, err, "Expected error when no active entry exists") 165 - AssertContains(t, err.Error(), "no active time entry found for task", "Expected specific error message") 166 }) 167 168 t.Run("GetByTaskID returns empty when no entries", func(t *testing.T) { ··· 171 task := createTestTask(t, db) 172 173 entries, err := repo.GetByTaskID(ctx, task.ID) 174 - AssertNoError(t, err, "Failed to get entries") 175 - AssertEqual(t, 0, len(entries), "Expected 0 entries") 176 }) 177 178 t.Run("GetByTaskID returns all entries for task", func(t *testing.T) { ··· 181 task := createTestTask(t, db) 182 183 _, err := repo.Start(ctx, task.ID, "First session") 184 - AssertNoError(t, err, "Failed to start first session") 185 186 _, err = repo.StopActiveByTaskID(ctx, task.ID) 187 - AssertNoError(t, err, "Failed to stop first session") 188 189 _, err = repo.Start(ctx, task.ID, "Second session") 190 - AssertNoError(t, err, "Failed to start second session") 191 192 entries, err := repo.GetByTaskID(ctx, task.ID) 193 - AssertNoError(t, err, "Failed to get entries") 194 - AssertEqual(t, 2, len(entries), "Expected 2 entries") 195 - AssertEqual(t, "Second session", entries[0].Description, "Expected newest entry first") 196 - AssertEqual(t, "First session", entries[1].Description, "Expected oldest entry second") 197 }) 198 199 t.Run("GetTotalTimeByTaskID returns zero when no entries", func(t *testing.T) { ··· 202 task := createTestTask(t, db) 203 204 duration, err := repo.GetTotalTimeByTaskID(ctx, task.ID) 205 - AssertNoError(t, err, "Failed to get total time") 206 - AssertEqual(t, time.Duration(0), duration, "Expected 0 duration") 207 }) 208 209 t.Run("GetTotalTimeByTaskID calculates total including active entries", func(t *testing.T) { ··· 212 task := createTestTask(t, db) 213 214 entry1, err := repo.Start(ctx, task.ID, "Completed work") 215 - AssertNoError(t, err, "Failed to start first entry") 216 217 time.Sleep(1010 * time.Millisecond) 218 _, err = repo.Stop(ctx, entry1.ID) 219 - AssertNoError(t, err, "Failed to stop first entry") 220 221 _, err = repo.Start(ctx, task.ID, "Active work") 222 - AssertNoError(t, err, "Failed to start second entry") 223 224 time.Sleep(1010 * time.Millisecond) 225 226 totalTime, err := repo.GetTotalTimeByTaskID(ctx, task.ID) 227 - AssertNoError(t, err, "Failed to get total time") 228 - AssertTrue(t, totalTime > 0, "Expected total time > 0") 229 - AssertTrue(t, totalTime >= 2*time.Second, "Expected total time >= 2s") 230 }) 231 }) 232 ··· 240 end := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC) 241 242 entries, err := repo.GetByDateRange(ctx, start, end) 243 - AssertNoError(t, err, "Failed to get entries by date range") 244 - AssertEqual(t, 0, len(entries), "Expected 0 entries") 245 }) 246 247 t.Run("Returns entries within date range", func(t *testing.T) { 248 task := createTestTask(t, db) 249 250 entry, err := repo.Start(ctx, task.ID, "Test entry") 251 - AssertNoError(t, err, "Failed to start entry") 252 253 _, err = repo.Stop(ctx, entry.ID) 254 - AssertNoError(t, err, "Failed to stop entry") 255 256 now := time.Now() 257 start := now.Add(-time.Hour) 258 end := now.Add(time.Hour) 259 260 entries, err := repo.GetByDateRange(ctx, start, end) 261 - AssertNoError(t, err, "Failed to get entries by date range") 262 263 found := false 264 for _, e := range entries { ··· 267 break 268 } 269 } 270 - AssertTrue(t, found, "Expected to find 'Test entry' in results") 271 }) 272 273 t.Run("Respects date range boundaries", func(t *testing.T) { 274 task := createTestTask(t, db) 275 276 entry, err := repo.Start(ctx, task.ID, "Boundary test") 277 - AssertNoError(t, err, "Failed to start entry") 278 279 _, err = repo.Stop(ctx, entry.ID) 280 - AssertNoError(t, err, "Failed to stop entry") 281 282 start := time.Now().Add(time.Hour) 283 end := time.Now().Add(2 * time.Hour) 284 285 entries, err := repo.GetByDateRange(ctx, start, end) 286 - AssertNoError(t, err, "Failed to get entries by date range") 287 288 for _, e := range entries { 289 if e.Description == "Boundary test" { ··· 297 end := time.Now().AddDate(0, 0, -1) 298 299 entries, err := repo.GetByDateRange(ctx, start, end) 300 - AssertNoError(t, err, "Should not error with invalid date range") 301 - AssertEqual(t, 0, len(entries), "Expected 0 entries with invalid range") 302 }) 303 }) 304 ··· 309 task := createTestTask(t, db) 310 311 entry, err := repo.Start(ctx, task.ID, "Test entry") 312 - AssertNoError(t, err, "Failed to create entry") 313 314 t.Run("Start with cancelled context", func(t *testing.T) { 315 db := CreateTestDB(t) ··· 371 372 t.Run("Get non-existent entry", func(t *testing.T) { 373 _, err := repo.Get(ctx, 99999) 374 - AssertError(t, err, "Expected error for non-existent entry") 375 - AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") 376 }) 377 378 t.Run("Stop non-existent entry", func(t *testing.T) { 379 _, err := repo.Stop(ctx, 99999) 380 - AssertError(t, err, "Expected error for non-existent entry") 381 }) 382 383 t.Run("Delete non-existent entry", func(t *testing.T) { 384 err := repo.Delete(ctx, 99999) 385 - AssertError(t, err, "Expected error for non-existent entry") 386 - AssertContains(t, err.Error(), "time entry not found", "Expected specific error message") 387 }) 388 389 t.Run("Start with non-existent task", func(t *testing.T) { 390 _, err := repo.Start(ctx, 99999, "Test") 391 - AssertError(t, err, "Expected error for non-existent task") 392 }) 393 394 t.Run("GetActiveByTaskID with no results", func(t *testing.T) { 395 task := createTestTask(t, db) 396 _, err := repo.GetActiveByTaskID(ctx, task.ID) 397 - AssertError(t, err, "Expected error when no active entry") 398 - AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") 399 }) 400 401 t.Run("GetByTaskID with no results", func(t *testing.T) { 402 task := createTestTask(t, db) 403 entries, err := repo.GetByTaskID(ctx, task.ID) 404 - AssertNoError(t, err, "Should not error when no entries found") 405 - AssertEqual(t, 0, len(entries), "Expected empty result set") 406 }) 407 }) 408 }
··· 9 10 _ "github.com/mattn/go-sqlite3" 11 "github.com/stormlightlabs/noteleaf/internal/models" 12 + "github.com/stormlightlabs/noteleaf/internal/shared" 13 ) 14 15 func createTestTask(t *testing.T, db *sql.DB) *models.Task { ··· 23 } 24 25 id, err := taskRepo.Create(ctx, task) 26 + shared.AssertNoError(t, err, "Failed to create test task") 27 task.ID = id 28 return task 29 } ··· 39 description := "Working on feature" 40 entry, err := repo.Start(ctx, task.ID, description) 41 42 + shared.AssertNoError(t, err, "Failed to start time tracking") 43 + shared.AssertNotEqual(t, int64(0), entry.ID, "Expected non-zero entry ID") 44 + shared.AssertEqual(t, task.ID, entry.TaskID, "Expected TaskID to match") 45 + shared.AssertEqual(t, description, entry.Description, "Expected description to match") 46 + shared.AssertTrue(t, entry.EndTime == nil, "Expected EndTime to be nil for active entry") 47 + shared.AssertTrue(t, entry.IsActive(), "Expected entry to be active") 48 }) 49 50 t.Run("Prevent starting already active task", func(t *testing.T) { 51 _, err := repo.Start(ctx, task.ID, "Another attempt") 52 53 + shared.AssertError(t, err, "Expected error when starting already active task") 54 + shared.AssertContains(t, err.Error(), "task already has an active time entry", "Expected specific error message") 55 }) 56 57 t.Run("Stop active time entry", func(t *testing.T) { ··· 60 task := createTestTask(t, db) 61 62 entry, err := repo.Start(ctx, task.ID, "Test work") 63 + shared.AssertNoError(t, err, "Failed to start time tracking") 64 65 time.Sleep(1010 * time.Millisecond) 66 67 stoppedEntry, err := repo.Stop(ctx, entry.ID) 68 + shared.AssertNoError(t, err, "Failed to stop time tracking") 69 + shared.AssertTrue(t, stoppedEntry.EndTime != nil, "Expected EndTime to be set") 70 + shared.AssertGreaterThan(t, stoppedEntry.DurationSeconds, int64(0), "Expected duration > 0") 71 + shared.AssertFalse(t, stoppedEntry.IsActive(), "Expected entry to not be active after stopping") 72 }) 73 74 t.Run("Fail to stop already stopped entry", func(t *testing.T) { ··· 77 task := createTestTask(t, db) 78 79 entry, err := repo.Start(ctx, task.ID, "Test work") 80 + shared.AssertNoError(t, err, "Failed to start time tracking") 81 82 time.Sleep(1010 * time.Millisecond) 83 _, err = repo.Stop(ctx, entry.ID) 84 + shared.AssertNoError(t, err, "Failed to stop time tracking") 85 86 _, err = repo.Stop(ctx, entry.ID) 87 + shared.AssertError(t, err, "Expected error when stopping already stopped entry") 88 + shared.AssertContains(t, err.Error(), "time entry is not active", "Expected specific error message") 89 }) 90 91 t.Run("Get time entry", func(t *testing.T) { ··· 94 task := createTestTask(t, db) 95 96 original, err := repo.Start(ctx, task.ID, "Test entry") 97 + shared.AssertNoError(t, err, "Failed to start time tracking") 98 99 retrieved, err := repo.Get(ctx, original.ID) 100 + shared.AssertNoError(t, err, "Failed to get time entry") 101 + shared.AssertEqual(t, original.ID, retrieved.ID, "ID mismatch") 102 + shared.AssertEqual(t, original.TaskID, retrieved.TaskID, "TaskID mismatch") 103 + shared.AssertEqual(t, original.Description, retrieved.Description, "Description mismatch") 104 }) 105 106 t.Run("Delete time entry", func(t *testing.T) { ··· 109 task := createTestTask(t, db) 110 111 entry, err := repo.Start(ctx, task.ID, "To be deleted") 112 + shared.AssertNoError(t, err, "Failed to create entry") 113 114 err = repo.Delete(ctx, entry.ID) 115 + shared.AssertNoError(t, err, "Failed to delete entry") 116 117 _, err = repo.Get(ctx, entry.ID) 118 + shared.AssertError(t, err, "Expected error when getting deleted entry") 119 + shared.AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") 120 }) 121 }) 122 ··· 128 129 t.Run("GetActiveByTaskID returns error when no active entry", func(t *testing.T) { 130 _, err := repo.GetActiveByTaskID(ctx, task.ID) 131 + shared.AssertError(t, err, "Expected error when no active entry exists") 132 + shared.AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") 133 }) 134 135 t.Run("GetActiveByTaskID returns active entry", func(t *testing.T) { 136 startedEntry, err := repo.Start(ctx, task.ID, "Test work") 137 + shared.AssertNoError(t, err, "Failed to start time tracking") 138 139 activeEntry, err := repo.GetActiveByTaskID(ctx, task.ID) 140 + shared.AssertNoError(t, err, "Failed to get active entry") 141 + shared.AssertEqual(t, startedEntry.ID, activeEntry.ID, "Expected entry IDs to match") 142 + shared.AssertTrue(t, activeEntry.IsActive(), "Expected entry to be active") 143 }) 144 145 t.Run("StopActiveByTaskID stops active entry", func(t *testing.T) { ··· 148 task := createTestTask(t, db) 149 150 _, err := repo.Start(ctx, task.ID, "Test work") 151 + shared.AssertNoError(t, err, "Failed to start time tracking") 152 153 stoppedEntry, err := repo.StopActiveByTaskID(ctx, task.ID) 154 + shared.AssertNoError(t, err, "Failed to stop time tracking by task ID") 155 + shared.AssertTrue(t, stoppedEntry.EndTime != nil, "Expected EndTime to be set") 156 + shared.AssertFalse(t, stoppedEntry.IsActive(), "Expected entry to not be active") 157 }) 158 159 t.Run("StopActiveByTaskID fails when no active entry", func(t *testing.T) { ··· 162 task := createTestTask(t, db) 163 164 _, err := repo.StopActiveByTaskID(ctx, task.ID) 165 + shared.AssertError(t, err, "Expected error when no active entry exists") 166 + shared.AssertContains(t, err.Error(), "no active time entry found for task", "Expected specific error message") 167 }) 168 169 t.Run("GetByTaskID returns empty when no entries", func(t *testing.T) { ··· 172 task := createTestTask(t, db) 173 174 entries, err := repo.GetByTaskID(ctx, task.ID) 175 + shared.AssertNoError(t, err, "Failed to get entries") 176 + shared.AssertEqual(t, 0, len(entries), "Expected 0 entries") 177 }) 178 179 t.Run("GetByTaskID returns all entries for task", func(t *testing.T) { ··· 182 task := createTestTask(t, db) 183 184 _, err := repo.Start(ctx, task.ID, "First session") 185 + shared.AssertNoError(t, err, "Failed to start first session") 186 187 _, err = repo.StopActiveByTaskID(ctx, task.ID) 188 + shared.AssertNoError(t, err, "Failed to stop first session") 189 190 _, err = repo.Start(ctx, task.ID, "Second session") 191 + shared.AssertNoError(t, err, "Failed to start second session") 192 193 entries, err := repo.GetByTaskID(ctx, task.ID) 194 + shared.AssertNoError(t, err, "Failed to get entries") 195 + shared.AssertEqual(t, 2, len(entries), "Expected 2 entries") 196 + shared.AssertEqual(t, "Second session", entries[0].Description, "Expected newest entry first") 197 + shared.AssertEqual(t, "First session", entries[1].Description, "Expected oldest entry second") 198 }) 199 200 t.Run("GetTotalTimeByTaskID returns zero when no entries", func(t *testing.T) { ··· 203 task := createTestTask(t, db) 204 205 duration, err := repo.GetTotalTimeByTaskID(ctx, task.ID) 206 + shared.AssertNoError(t, err, "Failed to get total time") 207 + shared.AssertEqual(t, time.Duration(0), duration, "Expected 0 duration") 208 }) 209 210 t.Run("GetTotalTimeByTaskID calculates total including active entries", func(t *testing.T) { ··· 213 task := createTestTask(t, db) 214 215 entry1, err := repo.Start(ctx, task.ID, "Completed work") 216 + shared.AssertNoError(t, err, "Failed to start first entry") 217 218 time.Sleep(1010 * time.Millisecond) 219 _, err = repo.Stop(ctx, entry1.ID) 220 + shared.AssertNoError(t, err, "Failed to stop first entry") 221 222 _, err = repo.Start(ctx, task.ID, "Active work") 223 + shared.AssertNoError(t, err, "Failed to start second entry") 224 225 time.Sleep(1010 * time.Millisecond) 226 227 totalTime, err := repo.GetTotalTimeByTaskID(ctx, task.ID) 228 + shared.AssertNoError(t, err, "Failed to get total time") 229 + shared.AssertTrue(t, totalTime > 0, "Expected total time > 0") 230 + shared.AssertTrue(t, totalTime >= 2*time.Second, "Expected total time >= 2s") 231 }) 232 }) 233 ··· 241 end := time.Date(2020, 1, 2, 0, 0, 0, 0, time.UTC) 242 243 entries, err := repo.GetByDateRange(ctx, start, end) 244 + shared.AssertNoError(t, err, "Failed to get entries by date range") 245 + shared.AssertEqual(t, 0, len(entries), "Expected 0 entries") 246 }) 247 248 t.Run("Returns entries within date range", func(t *testing.T) { 249 task := createTestTask(t, db) 250 251 entry, err := repo.Start(ctx, task.ID, "Test entry") 252 + shared.AssertNoError(t, err, "Failed to start entry") 253 254 _, err = repo.Stop(ctx, entry.ID) 255 + shared.AssertNoError(t, err, "Failed to stop entry") 256 257 now := time.Now() 258 start := now.Add(-time.Hour) 259 end := now.Add(time.Hour) 260 261 entries, err := repo.GetByDateRange(ctx, start, end) 262 + shared.AssertNoError(t, err, "Failed to get entries by date range") 263 264 found := false 265 for _, e := range entries { ··· 268 break 269 } 270 } 271 + shared.AssertTrue(t, found, "Expected to find 'Test entry' in results") 272 }) 273 274 t.Run("Respects date range boundaries", func(t *testing.T) { 275 task := createTestTask(t, db) 276 277 entry, err := repo.Start(ctx, task.ID, "Boundary test") 278 + shared.AssertNoError(t, err, "Failed to start entry") 279 280 _, err = repo.Stop(ctx, entry.ID) 281 + shared.AssertNoError(t, err, "Failed to stop entry") 282 283 start := time.Now().Add(time.Hour) 284 end := time.Now().Add(2 * time.Hour) 285 286 entries, err := repo.GetByDateRange(ctx, start, end) 287 + shared.AssertNoError(t, err, "Failed to get entries by date range") 288 289 for _, e := range entries { 290 if e.Description == "Boundary test" { ··· 298 end := time.Now().AddDate(0, 0, -1) 299 300 entries, err := repo.GetByDateRange(ctx, start, end) 301 + shared.AssertNoError(t, err, "Should not error with invalid date range") 302 + shared.AssertEqual(t, 0, len(entries), "Expected 0 entries with invalid range") 303 }) 304 }) 305 ··· 310 task := createTestTask(t, db) 311 312 entry, err := repo.Start(ctx, task.ID, "Test entry") 313 + shared.AssertNoError(t, err, "Failed to create entry") 314 315 t.Run("Start with cancelled context", func(t *testing.T) { 316 db := CreateTestDB(t) ··· 372 373 t.Run("Get non-existent entry", func(t *testing.T) { 374 _, err := repo.Get(ctx, 99999) 375 + shared.AssertError(t, err, "Expected error for non-existent entry") 376 + shared.AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") 377 }) 378 379 t.Run("Stop non-existent entry", func(t *testing.T) { 380 _, err := repo.Stop(ctx, 99999) 381 + shared.AssertError(t, err, "Expected error for non-existent entry") 382 }) 383 384 t.Run("Delete non-existent entry", func(t *testing.T) { 385 err := repo.Delete(ctx, 99999) 386 + shared.AssertError(t, err, "Expected error for non-existent entry") 387 + shared.AssertContains(t, err.Error(), "time entry not found", "Expected specific error message") 388 }) 389 390 t.Run("Start with non-existent task", func(t *testing.T) { 391 _, err := repo.Start(ctx, 99999, "Test") 392 + shared.AssertError(t, err, "Expected error for non-existent task") 393 }) 394 395 t.Run("GetActiveByTaskID with no results", func(t *testing.T) { 396 task := createTestTask(t, db) 397 _, err := repo.GetActiveByTaskID(ctx, task.ID) 398 + shared.AssertError(t, err, "Expected error when no active entry") 399 + shared.AssertEqual(t, sql.ErrNoRows, err, "Expected sql.ErrNoRows") 400 }) 401 402 t.Run("GetByTaskID with no results", func(t *testing.T) { 403 task := createTestTask(t, db) 404 entries, err := repo.GetByTaskID(ctx, task.ID) 405 + shared.AssertNoError(t, err, "Should not error when no entries found") 406 + shared.AssertEqual(t, 0, len(entries), "Expected empty result set") 407 }) 408 }) 409 }
+11 -10
internal/repo/tv_repository_test.go
··· 7 8 _ "github.com/mattn/go-sqlite3" 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 ) 11 12 func TestTVRepository(t *testing.T) { ··· 475 476 tvShow := NewTVShowBuilder().WithTitle("Test Show").WithSeason(1).WithEpisode(1).Build() 477 id, err := repo.Create(ctx, tvShow) 478 - AssertNoError(t, err, "Failed to create TV show") 479 480 t.Run("Create with cancelled context", func(t *testing.T) { 481 newShow := NewTVShowBuilder().WithTitle("Cancelled").Build() ··· 547 548 t.Run("Get non-existent TV show", func(t *testing.T) { 549 _, err := repo.Get(ctx, 99999) 550 - AssertError(t, err, "Expected error for non-existent TV show") 551 }) 552 553 t.Run("Update non-existent TV show succeeds with no rows affected", func(t *testing.T) { 554 show := NewTVShowBuilder().WithTitle("Non-existent").Build() 555 show.ID = 99999 556 err := repo.Update(ctx, show) 557 - AssertNoError(t, err, "Update should not error when no rows affected") 558 }) 559 560 t.Run("Delete non-existent TV show succeeds with no rows affected", func(t *testing.T) { 561 err := repo.Delete(ctx, 99999) 562 - AssertNoError(t, err, "Delete should not error when no rows affected") 563 }) 564 565 t.Run("MarkWatched non-existent TV show", func(t *testing.T) { 566 err := repo.MarkWatched(ctx, 99999) 567 - AssertError(t, err, "Expected error for non-existent TV show") 568 }) 569 570 t.Run("StartWatching non-existent TV show", func(t *testing.T) { 571 err := repo.StartWatching(ctx, 99999) 572 - AssertError(t, err, "Expected error for non-existent TV show") 573 }) 574 575 t.Run("GetByTitle with no results", func(t *testing.T) { 576 shows, err := repo.GetByTitle(ctx, "NonExistentShow") 577 - AssertNoError(t, err, "Should not error when no shows found") 578 - AssertEqual(t, 0, len(shows), "Expected empty result set") 579 }) 580 581 t.Run("GetBySeason with no results", func(t *testing.T) { 582 shows, err := repo.GetBySeason(ctx, "NonExistentShow", 1) 583 - AssertNoError(t, err, "Should not error when no shows found") 584 - AssertEqual(t, 0, len(shows), "Expected empty result set") 585 }) 586 }) 587 }
··· 7 8 _ "github.com/mattn/go-sqlite3" 9 "github.com/stormlightlabs/noteleaf/internal/models" 10 + "github.com/stormlightlabs/noteleaf/internal/shared" 11 ) 12 13 func TestTVRepository(t *testing.T) { ··· 476 477 tvShow := NewTVShowBuilder().WithTitle("Test Show").WithSeason(1).WithEpisode(1).Build() 478 id, err := repo.Create(ctx, tvShow) 479 + shared.AssertNoError(t, err, "Failed to create TV show") 480 481 t.Run("Create with cancelled context", func(t *testing.T) { 482 newShow := NewTVShowBuilder().WithTitle("Cancelled").Build() ··· 548 549 t.Run("Get non-existent TV show", func(t *testing.T) { 550 _, err := repo.Get(ctx, 99999) 551 + shared.AssertError(t, err, "Expected error for non-existent TV show") 552 }) 553 554 t.Run("Update non-existent TV show succeeds with no rows affected", func(t *testing.T) { 555 show := NewTVShowBuilder().WithTitle("Non-existent").Build() 556 show.ID = 99999 557 err := repo.Update(ctx, show) 558 + shared.AssertNoError(t, err, "Update should not error when no rows affected") 559 }) 560 561 t.Run("Delete non-existent TV show succeeds with no rows affected", func(t *testing.T) { 562 err := repo.Delete(ctx, 99999) 563 + shared.AssertNoError(t, err, "Delete should not error when no rows affected") 564 }) 565 566 t.Run("MarkWatched non-existent TV show", func(t *testing.T) { 567 err := repo.MarkWatched(ctx, 99999) 568 + shared.AssertError(t, err, "Expected error for non-existent TV show") 569 }) 570 571 t.Run("StartWatching non-existent TV show", func(t *testing.T) { 572 err := repo.StartWatching(ctx, 99999) 573 + shared.AssertError(t, err, "Expected error for non-existent TV show") 574 }) 575 576 t.Run("GetByTitle with no results", func(t *testing.T) { 577 shows, err := repo.GetByTitle(ctx, "NonExistentShow") 578 + shared.AssertNoError(t, err, "Should not error when no shows found") 579 + shared.AssertEqual(t, 0, len(shows), "Expected empty result set") 580 }) 581 582 t.Run("GetBySeason with no results", func(t *testing.T) { 583 shows, err := repo.GetBySeason(ctx, "NonExistentShow", 1) 584 + shared.AssertNoError(t, err, "Should not error when no shows found") 585 + shared.AssertEqual(t, 0, len(shows), "Expected empty result set") 586 }) 587 }) 588 }
+7 -7
internal/services/http_test.go
··· 21 ParseSearch = origSearch 22 }() 23 24 - tests := []struct { 25 name string 26 setup func() 27 call func() error ··· 165 }, 166 } 167 168 - for _, tc := range tests { 169 - t.Run(tc.name, func(t *testing.T) { 170 FetchHTML = origFetch 171 ExtractMovieMetadata = origMovie 172 ExtractTVSeriesMetadata = origTV 173 ExtractTVSeasonMetadata = origSeason 174 ParseSearch = origSearch 175 176 - tc.setup() 177 - err := tc.call() 178 - if tc.expectErr && err == nil { 179 t.Fatalf("expected error, got nil") 180 } 181 - if !tc.expectErr && err != nil { 182 t.Fatalf("unexpected error: %v", err) 183 } 184 })
··· 21 ParseSearch = origSearch 22 }() 23 24 + tc := []struct { 25 name string 26 setup func() 27 call func() error ··· 165 }, 166 } 167 168 + for _, tt := range tc { 169 + t.Run(tt.name, func(t *testing.T) { 170 FetchHTML = origFetch 171 ExtractMovieMetadata = origMovie 172 ExtractTVSeriesMetadata = origTV 173 ExtractTVSeasonMetadata = origSeason 174 ParseSearch = origSearch 175 176 + tt.setup() 177 + err := tt.call() 178 + if tt.expectErr && err == nil { 179 t.Fatalf("expected error, got nil") 180 } 181 + if !tt.expectErr && err != nil { 182 t.Fatalf("unexpected error: %v", err) 183 } 184 })
+19
internal/shared/shared.go
···
··· 1 + // package shared contains constants used across the codebase 2 + package shared 3 + 4 + import ( 5 + "errors" 6 + "fmt" 7 + ) 8 + 9 + var ( 10 + ErrConfig error = fmt.Errorf("configuration error") 11 + ) 12 + 13 + func ConfigError(m string, err error) error { 14 + return errors.Join(ErrConfig, fmt.Errorf("%s: %w", m, err)) 15 + } 16 + 17 + func IsConfigError(err error) bool { 18 + return errors.Is(err, ErrConfig) 19 + }
+70
internal/shared/shared_test.go
···
··· 1 + package shared 2 + 3 + import ( 4 + "errors" 5 + "testing" 6 + ) 7 + 8 + func TestErrors(t *testing.T) { 9 + t.Run("ConfigError", func(t *testing.T) { 10 + t.Run("creates joined error with message", func(t *testing.T) { 11 + baseErr := errors.New("invalid format") 12 + err := ConfigError("database connection failed", baseErr) 13 + 14 + AssertError(t, err, "ConfigError should create an error") 15 + AssertContains(t, err.Error(), "configuration error", "error should contain config error marker") 16 + AssertContains(t, err.Error(), "database connection failed", "error should contain custom message") 17 + AssertContains(t, err.Error(), "invalid format", "error should contain base error") 18 + }) 19 + 20 + t.Run("preserves both error chains", func(t *testing.T) { 21 + baseErr := errors.New("connection timeout") 22 + err := ConfigError("failed to connect", baseErr) 23 + 24 + AssertTrue(t, errors.Is(err, ErrConfig), "should identify as config error") 25 + AssertTrue(t, errors.Is(err, baseErr), "should preserve original error in chain") 26 + }) 27 + 28 + t.Run("wraps multiple errors with Join", func(t *testing.T) { 29 + baseErr := errors.New("parse error") 30 + err := ConfigError("invalid config file", baseErr) 31 + 32 + AssertTrue(t, errors.Is(err, ErrConfig), "joined error should contain ErrConfig") 33 + AssertTrue(t, errors.Is(err, baseErr), "joined error should contain base error") 34 + }) 35 + }) 36 + 37 + t.Run("IsConfigError", func(t *testing.T) { 38 + t.Run("identifies config errors", func(t *testing.T) { 39 + baseErr := errors.New("test error") 40 + err := ConfigError("test message", baseErr) 41 + 42 + AssertTrue(t, IsConfigError(err), "should identify config error") 43 + }) 44 + 45 + t.Run("returns false for regular errors", func(t *testing.T) { 46 + err := errors.New("regular error") 47 + 48 + AssertFalse(t, IsConfigError(err), "should not identify regular error as config error") 49 + }) 50 + 51 + t.Run("returns false for nil error", func(t *testing.T) { 52 + AssertFalse(t, IsConfigError(nil), "should return false for nil error") 53 + }) 54 + 55 + t.Run("returns false for wrapped non-config errors", func(t *testing.T) { 56 + baseErr := errors.New("base error") 57 + wrappedErr := errors.New("wrapped: " + baseErr.Error()) 58 + 59 + AssertFalse(t, IsConfigError(wrappedErr), "should not identify wrapped non-config error") 60 + }) 61 + 62 + t.Run("identifies wrapped config errors", func(t *testing.T) { 63 + baseErr := errors.New("original error") 64 + configErr := ConfigError("config issue", baseErr) 65 + wrappedAgain := errors.Join(errors.New("outer error"), configErr) 66 + 67 + AssertTrue(t, IsConfigError(wrappedAgain), "should identify config error in join chain") 68 + }) 69 + }) 70 + }
+238
internal/shared/test_utilities.go
···
··· 1 + // shared test utilities & helpers 2 + package shared 3 + 4 + import ( 5 + "encoding/json" 6 + "net/http" 7 + "net/http/httptest" 8 + "os" 9 + "strings" 10 + "testing" 11 + "time" 12 + ) 13 + 14 + func CreateTempDir(p string, t *testing.T) (string, func()) { 15 + t.Helper() 16 + tempDir, err := os.MkdirTemp("", p) 17 + if err != nil { 18 + t.Fatalf("Failed to create temp directory: %v", err) 19 + } 20 + return tempDir, func() { os.RemoveAll(tempDir) } 21 + } 22 + 23 + func AssertNoError(t *testing.T, err error, msg string) { 24 + t.Helper() 25 + if err != nil { 26 + t.Fatalf("%s: %v", msg, err) 27 + } 28 + } 29 + 30 + func AssertError(t *testing.T, err error, msg string) { 31 + t.Helper() 32 + if err == nil { 33 + t.Fatalf("%s: expected error but got none", msg) 34 + } 35 + } 36 + 37 + // AssertErrorContains checks that an error occurred and optionally contains expected text 38 + func AssertErrorContains(t *testing.T, err error, expected, msg string) { 39 + t.Helper() 40 + if err == nil { 41 + t.Errorf("%s: expected error but got none", msg) 42 + return 43 + } 44 + if expected != "" && !ContainsString(err.Error(), expected) { 45 + t.Errorf("%s: expected error containing %q, got: %v", msg, expected, err) 46 + } 47 + } 48 + 49 + func AssertTrue(t *testing.T, condition bool, msg string) { 50 + t.Helper() 51 + if !condition { 52 + t.Fatalf("%s: expected true", msg) 53 + } 54 + } 55 + 56 + func AssertFalse(t *testing.T, condition bool, msg string) { 57 + t.Helper() 58 + if condition { 59 + t.Fatalf("%s: expected false", msg) 60 + } 61 + } 62 + 63 + func AssertContains(t *testing.T, str, substr, msg string) { 64 + t.Helper() 65 + if !strings.Contains(str, substr) { 66 + t.Fatalf("%s: expected string '%s' to contain '%s'", msg, str, substr) 67 + } 68 + } 69 + 70 + func AssertEqual[T comparable](t *testing.T, expected, actual T, msg string) { 71 + t.Helper() 72 + if expected != actual { 73 + t.Fatalf("%s: expected %v, got %v", msg, expected, actual) 74 + } 75 + } 76 + 77 + func AssertNotEqual[T comparable](t *testing.T, not, actual T, msg string) { 78 + t.Helper() 79 + if not == actual { 80 + t.Fatalf("%s: expected value to not equal %v", msg, not) 81 + } 82 + } 83 + 84 + func AssertNil(t *testing.T, value any, msg string) { 85 + t.Helper() 86 + if value != nil { 87 + t.Fatalf("%s: expected nil, got %v", msg, value) 88 + } 89 + } 90 + 91 + func AssertNotNil(t *testing.T, value any, msg string) { 92 + t.Helper() 93 + if value == nil { 94 + t.Fatalf("%s: expected non-nil value", msg) 95 + } 96 + } 97 + 98 + func AssertGreaterThan[T interface{ int | int64 | float64 }](t *testing.T, actual, threshold T, msg string) { 99 + t.Helper() 100 + if actual <= threshold { 101 + t.Fatalf("%s: expected %v > %v", msg, actual, threshold) 102 + } 103 + } 104 + 105 + func AssertLessThan[T interface{ int | int64 | float64 }](t *testing.T, actual, threshold T, msg string) { 106 + t.Helper() 107 + if actual >= threshold { 108 + t.Fatalf("%s: expected %v < %v", msg, actual, threshold) 109 + } 110 + } 111 + 112 + // Helper function to check if string contains substring (case-insensitive) 113 + func ContainsString(haystack, needle string) bool { 114 + if needle == "" { 115 + return true 116 + } 117 + return len(haystack) >= len(needle) && 118 + haystack[len(haystack)-len(needle):] == needle || 119 + haystack[:len(needle)] == needle || 120 + (len(haystack) > len(needle) && 121 + func() bool { 122 + for i := 1; i <= len(haystack)-len(needle); i++ { 123 + if haystack[i:i+len(needle)] == needle { 124 + return true 125 + } 126 + } 127 + return false 128 + }()) 129 + } 130 + 131 + // HTTPMockServer provides utilities for mocking HTTP services in tests 132 + type HTTPMockServer struct { 133 + server *httptest.Server 134 + requests []*http.Request 135 + } 136 + 137 + // NewMockServer creates a new mock HTTP server 138 + func NewMockServer() *HTTPMockServer { 139 + mock := &HTTPMockServer{ 140 + requests: make([]*http.Request, 0), 141 + } 142 + return mock 143 + } 144 + 145 + // WithHandler sets up the mock server with a custom handler 146 + func (m *HTTPMockServer) WithHandler(handler http.HandlerFunc) *HTTPMockServer { 147 + m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 148 + m.requests = append(m.requests, r) 149 + handler(w, r) 150 + })) 151 + return m 152 + } 153 + 154 + // URL returns the mock server URL 155 + func (m *HTTPMockServer) URL() string { 156 + if m.server == nil { 157 + panic("mock server not initialized - call WithHandler first") 158 + } 159 + return m.server.URL 160 + } 161 + 162 + // Close closes the mock server 163 + func (m *HTTPMockServer) Close() { 164 + if m.server != nil { 165 + m.server.Close() 166 + } 167 + } 168 + 169 + // GetRequests returns all recorded HTTP requests 170 + func (m *HTTPMockServer) GetRequests() []*http.Request { 171 + return m.requests 172 + } 173 + 174 + // GetLastRequest returns the last recorded HTTP request 175 + func (m *HTTPMockServer) GetLastRequest() *http.Request { 176 + if len(m.requests) == 0 { 177 + return nil 178 + } 179 + return m.requests[len(m.requests)-1] 180 + } 181 + 182 + func (m HTTPMockServer) Requests() []*http.Request { 183 + return m.requests 184 + } 185 + 186 + // HTTPErrorMockServer creates a mock server that returns HTTP errors 187 + func HTTPErrorMockServer(statusCode int, message string) *HTTPMockServer { 188 + return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 189 + http.Error(w, message, statusCode) 190 + }) 191 + } 192 + 193 + // JSONMockServer creates a mock server that returns JSON responses 194 + func JSONMockServer(response any) *HTTPMockServer { 195 + return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 196 + w.Header().Set("Content-Type", "application/json") 197 + if err := json.NewEncoder(w).Encode(response); err != nil { 198 + http.Error(w, "Failed to encode response", http.StatusInternalServerError) 199 + } 200 + }) 201 + } 202 + 203 + // TimeoutMockServer creates a mock server that simulates timeouts 204 + func TimeoutMockServer(delay time.Duration) *HTTPMockServer { 205 + return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 206 + time.Sleep(delay) 207 + w.WriteHeader(http.StatusOK) 208 + }) 209 + } 210 + 211 + // InvalidJSONMockServer creates a mock server that returns malformed JSON 212 + func InvalidJSONMockServer() *HTTPMockServer { 213 + return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 214 + w.Header().Set("Content-Type", "application/json") 215 + w.Write([]byte(`{"invalid": json`)) 216 + }) 217 + } 218 + 219 + // EmptyResponseMockServer creates a mock server that returns empty responses 220 + func EmptyResponseMockServer() *HTTPMockServer { 221 + return NewMockServer().WithHandler(func(w http.ResponseWriter, r *http.Request) { 222 + w.WriteHeader(http.StatusOK) 223 + }) 224 + } 225 + 226 + // AssertRequestMade verifies that a request was made to the mock server 227 + func AssertRequestMade(t *testing.T, server *HTTPMockServer, expected string) { 228 + t.Helper() 229 + if len(server.requests) == 0 { 230 + t.Error("Expected HTTP request to be made but none were recorded") 231 + return 232 + } 233 + 234 + lastReq := server.GetLastRequest() 235 + if lastReq.URL.Path != expected { 236 + t.Errorf("Expected request to path %s, got %s", expected, lastReq.URL.Path) 237 + } 238 + }
+9 -12
internal/store/config.go
··· 1 package store 2 3 import ( 4 - "fmt" 5 "os" 6 "path/filepath" 7 8 "github.com/BurntSushi/toml" 9 ) 10 11 // Config holds application configuration ··· 49 } else { 50 configDir, err := GetConfigDir() 51 if err != nil { 52 - return nil, fmt.Errorf("failed to get config directory: %w", err) 53 } 54 configPath = filepath.Join(configDir, ".noteleaf.conf.toml") 55 } ··· 57 if _, err := os.Stat(configPath); os.IsNotExist(err) { 58 config := DefaultConfig() 59 if err := SaveConfig(config); err != nil { 60 - return nil, fmt.Errorf("failed to create default config: %w", err) 61 } 62 return config, nil 63 } 64 65 data, err := os.ReadFile(configPath) 66 if err != nil { 67 - return nil, fmt.Errorf("failed to read config file: %w", err) 68 } 69 70 config := DefaultConfig() 71 if err := toml.Unmarshal(data, config); err != nil { 72 - return nil, fmt.Errorf("failed to parse config file: %w", err) 73 } 74 75 return config, nil ··· 79 func SaveConfig(config *Config) error { 80 var configPath string 81 82 - // Check for NOTELEAF_CONFIG environment variable 83 if envConfigPath := os.Getenv("NOTELEAF_CONFIG"); envConfigPath != "" { 84 configPath = envConfigPath 85 - // Ensure the directory exists for custom config path 86 configDir := filepath.Dir(configPath) 87 if err := os.MkdirAll(configDir, 0755); err != nil { 88 - return fmt.Errorf("failed to create config directory: %w", err) 89 } 90 } else { 91 configDir, err := GetConfigDir() 92 if err != nil { 93 - return fmt.Errorf("failed to get config directory: %w", err) 94 } 95 configPath = filepath.Join(configDir, ".noteleaf.conf.toml") 96 } 97 98 data, err := toml.Marshal(config) 99 if err != nil { 100 - return fmt.Errorf("failed to marshal config: %w", err) 101 } 102 103 if err := os.WriteFile(configPath, data, 0644); err != nil { 104 - return fmt.Errorf("failed to write config file: %w", err) 105 } 106 107 return nil ··· 109 110 // GetConfigPath returns the path to the configuration file 111 func GetConfigPath() (string, error) { 112 - // Check for NOTELEAF_CONFIG environment variable 113 if envConfigPath := os.Getenv("NOTELEAF_CONFIG"); envConfigPath != "" { 114 return envConfigPath, nil 115 }
··· 1 package store 2 3 import ( 4 "os" 5 "path/filepath" 6 7 "github.com/BurntSushi/toml" 8 + "github.com/stormlightlabs/noteleaf/internal/shared" 9 ) 10 11 // Config holds application configuration ··· 49 } else { 50 configDir, err := GetConfigDir() 51 if err != nil { 52 + return nil, shared.ConfigError("failed to get config directory", err) 53 } 54 configPath = filepath.Join(configDir, ".noteleaf.conf.toml") 55 } ··· 57 if _, err := os.Stat(configPath); os.IsNotExist(err) { 58 config := DefaultConfig() 59 if err := SaveConfig(config); err != nil { 60 + return nil, shared.ConfigError("failed to create default config", err) 61 } 62 return config, nil 63 } 64 65 data, err := os.ReadFile(configPath) 66 if err != nil { 67 + return nil, shared.ConfigError("failed to read config file", err) 68 } 69 70 config := DefaultConfig() 71 if err := toml.Unmarshal(data, config); err != nil { 72 + return nil, shared.ConfigError("failed to parse config file", err) 73 } 74 75 return config, nil ··· 79 func SaveConfig(config *Config) error { 80 var configPath string 81 82 if envConfigPath := os.Getenv("NOTELEAF_CONFIG"); envConfigPath != "" { 83 configPath = envConfigPath 84 configDir := filepath.Dir(configPath) 85 if err := os.MkdirAll(configDir, 0755); err != nil { 86 + return shared.ConfigError("failed to create config directory", err) 87 } 88 } else { 89 configDir, err := GetConfigDir() 90 if err != nil { 91 + return shared.ConfigError("failed to get config directory", err) 92 } 93 configPath = filepath.Join(configDir, ".noteleaf.conf.toml") 94 } 95 96 data, err := toml.Marshal(config) 97 if err != nil { 98 + return shared.ConfigError("failed to marshal config", err) 99 } 100 101 if err := os.WriteFile(configPath, data, 0644); err != nil { 102 + return shared.ConfigError("failed to write config file", err) 103 } 104 105 return nil ··· 107 108 // GetConfigPath returns the path to the configuration file 109 func GetConfigPath() (string, error) { 110 if envConfigPath := os.Getenv("NOTELEAF_CONFIG"); envConfigPath != "" { 111 return envConfigPath, nil 112 }
+36 -93
internal/store/config_test.go
··· 8 "testing" 9 10 "github.com/BurntSushi/toml" 11 ) 12 13 func TestDefaultConfig(t *testing.T) { ··· 47 } 48 49 func TestConfigOperations(t *testing.T) { 50 - tempDir, err := os.MkdirTemp("", "noteleaf-config-test-*") 51 - if err != nil { 52 - t.Fatalf("Failed to create temp directory: %v", err) 53 - } 54 - defer os.RemoveAll(tempDir) 55 56 originalGetConfigDir := GetConfigDir 57 GetConfigDir = func() (string, error) { ··· 124 } 125 126 func TestConfigPersistence(t *testing.T) { 127 - tempDir, err := os.MkdirTemp("", "noteleaf-config-persist-test-*") 128 - if err != nil { 129 - t.Fatalf("Failed to create temp directory: %v", err) 130 - } 131 - defer os.RemoveAll(tempDir) 132 133 originalGetConfigDir := GetConfigDir 134 GetConfigDir = func() (string, error) { ··· 199 200 func TestConfigErrorHandling(t *testing.T) { 201 t.Run("LoadConfig handles invalid TOML", func(t *testing.T) { 202 - tempDir, err := os.MkdirTemp("", "noteleaf-config-error-test-*") 203 - if err != nil { 204 - t.Fatalf("Failed to create temp directory: %v", err) 205 - } 206 - defer os.RemoveAll(tempDir) 207 208 originalGetConfigDir := GetConfigDir 209 GetConfigDir = func() (string, error) { ··· 213 214 configPath := filepath.Join(tempDir, ".noteleaf.conf.toml") 215 invalidTOML := `[invalid toml content` 216 - err = os.WriteFile(configPath, []byte(invalidTOML), 0644) 217 - if err != nil { 218 t.Fatalf("Failed to write invalid TOML: %v", err) 219 } 220 221 - _, err = LoadConfig() 222 - if err == nil { 223 t.Error("LoadConfig should fail with invalid TOML") 224 } 225 }) ··· 229 t.Skip("Permission test not reliable on Windows") 230 } 231 232 - tempDir, err := os.MkdirTemp("", "noteleaf-config-perm-test-*") 233 - if err != nil { 234 - t.Fatalf("Failed to create temp directory: %v", err) 235 - } 236 - defer os.RemoveAll(tempDir) 237 238 originalGetConfigDir := GetConfigDir 239 GetConfigDir = func() (string, error) { ··· 243 244 configPath := filepath.Join(tempDir, ".noteleaf.conf.toml") 245 validTOML := `color_scheme = "dark"` 246 - err = os.WriteFile(configPath, []byte(validTOML), 0644) 247 - if err != nil { 248 t.Fatalf("Failed to write config file: %v", err) 249 } 250 251 - err = os.Chmod(configPath, 0000) 252 - if err != nil { 253 t.Fatalf("Failed to change file permissions: %v", err) 254 } 255 defer os.Chmod(configPath, 0644) 256 257 - _, err = LoadConfig() 258 - if err == nil { 259 t.Error("LoadConfig should fail when config file is not readable") 260 } 261 }) ··· 274 }) 275 276 t.Run("LoadConfig handles SaveConfig failure when creating default", func(t *testing.T) { 277 - tempDir, err := os.MkdirTemp("", "noteleaf-config-save-fail-test-*") 278 - if err != nil { 279 - t.Fatalf("Failed to create temp directory: %v", err) 280 - } 281 - defer os.RemoveAll(tempDir) 282 283 _ = filepath.Join(tempDir, ".noteleaf.conf.toml") 284 ··· 293 } 294 defer func() { GetConfigDir = originalGetConfigDir }() 295 296 - _, err = LoadConfig() 297 - if err == nil { 298 t.Error("LoadConfig should fail when SaveConfig fails during default config creation") 299 } 300 }) ··· 307 defer func() { GetConfigDir = originalGetConfigDir }() 308 309 config := DefaultConfig() 310 - err := SaveConfig(config) 311 - if err == nil { 312 t.Error("SaveConfig should fail when config directory cannot be accessed") 313 } 314 }) ··· 318 t.Skip("Permission test not reliable on Windows") 319 } 320 321 - tempDir, err := os.MkdirTemp("", "noteleaf-config-write-perm-test-*") 322 - if err != nil { 323 - t.Fatalf("Failed to create temp directory: %v", err) 324 - } 325 - defer os.RemoveAll(tempDir) 326 327 originalGetConfigDir := GetConfigDir 328 GetConfigDir = func() (string, error) { ··· 330 } 331 defer func() { GetConfigDir = originalGetConfigDir }() 332 333 - err = os.Chmod(tempDir, 0555) 334 - if err != nil { 335 t.Fatalf("Failed to change directory permissions: %v", err) 336 } 337 defer os.Chmod(tempDir, 0755) 338 339 config := DefaultConfig() 340 - err = SaveConfig(config) 341 - if err == nil { 342 t.Error("SaveConfig should fail when directory is not writable") 343 } 344 }) ··· 378 }) 379 380 t.Run("creates directory if it doesn't exist", func(t *testing.T) { 381 - tempDir, err := os.MkdirTemp("", "noteleaf-test-*") 382 - if err != nil { 383 - t.Fatalf("Failed to create temp directory: %v", err) 384 - } 385 - defer os.RemoveAll(tempDir) 386 387 var originalEnv string 388 var envVar string ··· 537 538 func TestEnvironmentVariableOverrides(t *testing.T) { 539 t.Run("NOTELEAF_CONFIG overrides default config path for LoadConfig", func(t *testing.T) { 540 - tempDir, err := os.MkdirTemp("", "noteleaf-env-config-test-*") 541 - if err != nil { 542 - t.Fatalf("Failed to create temp directory: %v", err) 543 - } 544 - defer os.RemoveAll(tempDir) 545 546 customConfigPath := filepath.Join(tempDir, "custom-config.toml") 547 originalEnv := os.Getenv("NOTELEAF_CONFIG") 548 os.Setenv("NOTELEAF_CONFIG", customConfigPath) 549 defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 550 551 - // Create a custom config 552 customConfig := DefaultConfig() 553 customConfig.ColorScheme = "custom-env-test" 554 if err := SaveConfig(customConfig); err != nil { 555 t.Fatalf("Failed to save custom config: %v", err) 556 } 557 558 - // Load config should use the custom path 559 loadedConfig, err := LoadConfig() 560 if err != nil { 561 t.Fatalf("LoadConfig failed: %v", err) ··· 567 }) 568 569 t.Run("NOTELEAF_CONFIG overrides default config path for SaveConfig", func(t *testing.T) { 570 - tempDir, err := os.MkdirTemp("", "noteleaf-env-save-test-*") 571 - if err != nil { 572 - t.Fatalf("Failed to create temp directory: %v", err) 573 - } 574 - defer os.RemoveAll(tempDir) 575 576 customConfigPath := filepath.Join(tempDir, "subdir", "config.toml") 577 originalEnv := os.Getenv("NOTELEAF_CONFIG") ··· 584 t.Fatalf("SaveConfig failed: %v", err) 585 } 586 587 - // Verify the file was created at the custom path 588 if _, err := os.Stat(customConfigPath); os.IsNotExist(err) { 589 t.Error("Config file should be created at custom NOTELEAF_CONFIG path") 590 } 591 592 - // Verify the content 593 data, err := os.ReadFile(customConfigPath) 594 if err != nil { 595 t.Fatalf("Failed to read config file: %v", err) ··· 606 }) 607 608 t.Run("NOTELEAF_CONFIG overrides default config path for GetConfigPath", func(t *testing.T) { 609 - tempDir, err := os.MkdirTemp("", "noteleaf-env-path-test-*") 610 - if err != nil { 611 - t.Fatalf("Failed to create temp directory: %v", err) 612 - } 613 - defer os.RemoveAll(tempDir) 614 615 customConfigPath := filepath.Join(tempDir, "my-config.toml") 616 originalEnv := os.Getenv("NOTELEAF_CONFIG") ··· 628 }) 629 630 t.Run("NOTELEAF_CONFIG creates parent directories if needed", func(t *testing.T) { 631 - tempDir, err := os.MkdirTemp("", "noteleaf-env-mkdir-test-*") 632 - if err != nil { 633 - t.Fatalf("Failed to create temp directory: %v", err) 634 - } 635 - defer os.RemoveAll(tempDir) 636 637 customConfigPath := filepath.Join(tempDir, "nested", "deep", "config.toml") 638 originalEnv := os.Getenv("NOTELEAF_CONFIG") ··· 652 653 func TestGetDataDir(t *testing.T) { 654 t.Run("NOTELEAF_DATA_DIR overrides default data directory", func(t *testing.T) { 655 - tempDir, err := os.MkdirTemp("", "noteleaf-data-dir-test-*") 656 - if err != nil { 657 - t.Fatalf("Failed to create temp directory: %v", err) 658 - } 659 - defer os.RemoveAll(tempDir) 660 661 customDataDir := filepath.Join(tempDir, "my-data") 662 originalEnv := os.Getenv("NOTELEAF_DATA_DIR") ··· 672 t.Errorf("Expected data dir '%s', got '%s'", customDataDir, dataDir) 673 } 674 675 - // Verify directory was created 676 if _, err := os.Stat(customDataDir); os.IsNotExist(err) { 677 t.Error("Data directory should be created") 678 } 679 }) 680 681 t.Run("GetDataDir returns correct directory based on OS", func(t *testing.T) { 682 - // Temporarily unset NOTELEAF_DATA_DIR 683 originalEnv := os.Getenv("NOTELEAF_DATA_DIR") 684 os.Unsetenv("NOTELEAF_DATA_DIR") 685 defer os.Setenv("NOTELEAF_DATA_DIR", originalEnv) ··· 699 }) 700 701 t.Run("GetDataDir handles NOTELEAF_DATA_DIR with nested path", func(t *testing.T) { 702 - tempDir, err := os.MkdirTemp("", "noteleaf-nested-data-test-*") 703 - if err != nil { 704 - t.Fatalf("Failed to create temp directory: %v", err) 705 - } 706 - defer os.RemoveAll(tempDir) 707 708 customDataDir := filepath.Join(tempDir, "level1", "level2", "data") 709 originalEnv := os.Getenv("NOTELEAF_DATA_DIR") ··· 719 t.Errorf("Expected data dir '%s', got '%s'", customDataDir, dataDir) 720 } 721 722 - // Verify nested directories were created 723 if _, err := os.Stat(customDataDir); os.IsNotExist(err) { 724 t.Error("Nested data directories should be created") 725 } 726 }) 727 728 t.Run("GetDataDir uses platform-specific defaults", func(t *testing.T) { 729 - // Temporarily unset NOTELEAF_DATA_DIR 730 originalEnv := os.Getenv("NOTELEAF_DATA_DIR") 731 os.Unsetenv("NOTELEAF_DATA_DIR") 732 defer os.Setenv("NOTELEAF_DATA_DIR", originalEnv) 733 734 - // Create temporary environment for testing 735 tempHome, err := os.MkdirTemp("", "noteleaf-home-test-*") 736 if err != nil { 737 t.Fatalf("Failed to create temp home: %v", err) ··· 760 t.Fatalf("GetDataDir failed: %v", err) 761 } 762 763 - // Verify the path contains our temp directory 764 if !strings.Contains(dataDir, tempHome) { 765 t.Errorf("Data directory should be under temp home, got: %s", dataDir) 766 }
··· 8 "testing" 9 10 "github.com/BurntSushi/toml" 11 + "github.com/stormlightlabs/noteleaf/internal/shared" 12 ) 13 14 func TestDefaultConfig(t *testing.T) { ··· 48 } 49 50 func TestConfigOperations(t *testing.T) { 51 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-test-*", t) 52 + defer cleanup() 53 54 originalGetConfigDir := GetConfigDir 55 GetConfigDir = func() (string, error) { ··· 122 } 123 124 func TestConfigPersistence(t *testing.T) { 125 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-persist-test-*", t) 126 + defer cleanup() 127 128 originalGetConfigDir := GetConfigDir 129 GetConfigDir = func() (string, error) { ··· 194 195 func TestConfigErrorHandling(t *testing.T) { 196 t.Run("LoadConfig handles invalid TOML", func(t *testing.T) { 197 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-error-test-*", t) 198 + defer cleanup() 199 200 originalGetConfigDir := GetConfigDir 201 GetConfigDir = func() (string, error) { ··· 205 206 configPath := filepath.Join(tempDir, ".noteleaf.conf.toml") 207 invalidTOML := `[invalid toml content` 208 + if err := os.WriteFile(configPath, []byte(invalidTOML), 0644); err != nil { 209 t.Fatalf("Failed to write invalid TOML: %v", err) 210 } 211 212 + if _, err := LoadConfig(); err == nil { 213 t.Error("LoadConfig should fail with invalid TOML") 214 } 215 }) ··· 219 t.Skip("Permission test not reliable on Windows") 220 } 221 222 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-perm-test-*", t) 223 + defer cleanup() 224 225 originalGetConfigDir := GetConfigDir 226 GetConfigDir = func() (string, error) { ··· 230 231 configPath := filepath.Join(tempDir, ".noteleaf.conf.toml") 232 validTOML := `color_scheme = "dark"` 233 + if err := os.WriteFile(configPath, []byte(validTOML), 0644); err != nil { 234 t.Fatalf("Failed to write config file: %v", err) 235 } 236 237 + if err := os.Chmod(configPath, 0000); err != nil { 238 t.Fatalf("Failed to change file permissions: %v", err) 239 } 240 defer os.Chmod(configPath, 0644) 241 242 + if _, err := LoadConfig(); err == nil { 243 t.Error("LoadConfig should fail when config file is not readable") 244 } 245 }) ··· 258 }) 259 260 t.Run("LoadConfig handles SaveConfig failure when creating default", func(t *testing.T) { 261 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-save-fail-test-*", t) 262 + defer cleanup() 263 264 _ = filepath.Join(tempDir, ".noteleaf.conf.toml") 265 ··· 274 } 275 defer func() { GetConfigDir = originalGetConfigDir }() 276 277 + if _, err := LoadConfig(); err == nil { 278 t.Error("LoadConfig should fail when SaveConfig fails during default config creation") 279 } 280 }) ··· 287 defer func() { GetConfigDir = originalGetConfigDir }() 288 289 config := DefaultConfig() 290 + if err := SaveConfig(config); err == nil { 291 t.Error("SaveConfig should fail when config directory cannot be accessed") 292 } 293 }) ··· 297 t.Skip("Permission test not reliable on Windows") 298 } 299 300 + tempDir, cleanup := shared.CreateTempDir("noteleaf-config-write-perm-test-*", t) 301 + defer cleanup() 302 303 originalGetConfigDir := GetConfigDir 304 GetConfigDir = func() (string, error) { ··· 306 } 307 defer func() { GetConfigDir = originalGetConfigDir }() 308 309 + if err := os.Chmod(tempDir, 0555); err != nil { 310 t.Fatalf("Failed to change directory permissions: %v", err) 311 } 312 defer os.Chmod(tempDir, 0755) 313 314 config := DefaultConfig() 315 + if err := SaveConfig(config); err == nil { 316 t.Error("SaveConfig should fail when directory is not writable") 317 } 318 }) ··· 352 }) 353 354 t.Run("creates directory if it doesn't exist", func(t *testing.T) { 355 + tempDir, cleanup := shared.CreateTempDir("noteleaf-test-*", t) 356 + defer cleanup() 357 358 var originalEnv string 359 var envVar string ··· 508 509 func TestEnvironmentVariableOverrides(t *testing.T) { 510 t.Run("NOTELEAF_CONFIG overrides default config path for LoadConfig", func(t *testing.T) { 511 + tempDir, cleanup := shared.CreateTempDir("noteleaf-env-config-test-*", t) 512 + defer cleanup() 513 514 customConfigPath := filepath.Join(tempDir, "custom-config.toml") 515 originalEnv := os.Getenv("NOTELEAF_CONFIG") 516 os.Setenv("NOTELEAF_CONFIG", customConfigPath) 517 defer os.Setenv("NOTELEAF_CONFIG", originalEnv) 518 519 customConfig := DefaultConfig() 520 customConfig.ColorScheme = "custom-env-test" 521 if err := SaveConfig(customConfig); err != nil { 522 t.Fatalf("Failed to save custom config: %v", err) 523 } 524 525 loadedConfig, err := LoadConfig() 526 if err != nil { 527 t.Fatalf("LoadConfig failed: %v", err) ··· 533 }) 534 535 t.Run("NOTELEAF_CONFIG overrides default config path for SaveConfig", func(t *testing.T) { 536 + tempDir, cleanup := shared.CreateTempDir("noteleaf-env-save-test-*", t) 537 + defer cleanup() 538 539 customConfigPath := filepath.Join(tempDir, "subdir", "config.toml") 540 originalEnv := os.Getenv("NOTELEAF_CONFIG") ··· 547 t.Fatalf("SaveConfig failed: %v", err) 548 } 549 550 if _, err := os.Stat(customConfigPath); os.IsNotExist(err) { 551 t.Error("Config file should be created at custom NOTELEAF_CONFIG path") 552 } 553 554 data, err := os.ReadFile(customConfigPath) 555 if err != nil { 556 t.Fatalf("Failed to read config file: %v", err) ··· 567 }) 568 569 t.Run("NOTELEAF_CONFIG overrides default config path for GetConfigPath", func(t *testing.T) { 570 + tempDir, cleanup := shared.CreateTempDir("noteleaf-env-path-test-*", t) 571 + defer cleanup() 572 573 customConfigPath := filepath.Join(tempDir, "my-config.toml") 574 originalEnv := os.Getenv("NOTELEAF_CONFIG") ··· 586 }) 587 588 t.Run("NOTELEAF_CONFIG creates parent directories if needed", func(t *testing.T) { 589 + tempDir, cleanup := shared.CreateTempDir("noteleaf-env-mkdir-test-*", t) 590 + defer cleanup() 591 592 customConfigPath := filepath.Join(tempDir, "nested", "deep", "config.toml") 593 originalEnv := os.Getenv("NOTELEAF_CONFIG") ··· 607 608 func TestGetDataDir(t *testing.T) { 609 t.Run("NOTELEAF_DATA_DIR overrides default data directory", func(t *testing.T) { 610 + tempDir, cleanup := shared.CreateTempDir("noteleaf-data-dir-test-*", t) 611 + defer cleanup() 612 613 customDataDir := filepath.Join(tempDir, "my-data") 614 originalEnv := os.Getenv("NOTELEAF_DATA_DIR") ··· 624 t.Errorf("Expected data dir '%s', got '%s'", customDataDir, dataDir) 625 } 626 627 if _, err := os.Stat(customDataDir); os.IsNotExist(err) { 628 t.Error("Data directory should be created") 629 } 630 }) 631 632 t.Run("GetDataDir returns correct directory based on OS", func(t *testing.T) { 633 originalEnv := os.Getenv("NOTELEAF_DATA_DIR") 634 os.Unsetenv("NOTELEAF_DATA_DIR") 635 defer os.Setenv("NOTELEAF_DATA_DIR", originalEnv) ··· 649 }) 650 651 t.Run("GetDataDir handles NOTELEAF_DATA_DIR with nested path", func(t *testing.T) { 652 + tempDir, cleanup := shared.CreateTempDir("noteleaf-nested-data-test-*", t) 653 + defer cleanup() 654 655 customDataDir := filepath.Join(tempDir, "level1", "level2", "data") 656 originalEnv := os.Getenv("NOTELEAF_DATA_DIR") ··· 666 t.Errorf("Expected data dir '%s', got '%s'", customDataDir, dataDir) 667 } 668 669 if _, err := os.Stat(customDataDir); os.IsNotExist(err) { 670 t.Error("Nested data directories should be created") 671 } 672 }) 673 674 t.Run("GetDataDir uses platform-specific defaults", func(t *testing.T) { 675 originalEnv := os.Getenv("NOTELEAF_DATA_DIR") 676 os.Unsetenv("NOTELEAF_DATA_DIR") 677 defer os.Setenv("NOTELEAF_DATA_DIR", originalEnv) 678 679 tempHome, err := os.MkdirTemp("", "noteleaf-home-test-*") 680 if err != nil { 681 t.Fatalf("Failed to create temp home: %v", err) ··· 704 t.Fatalf("GetDataDir failed: %v", err) 705 } 706 707 if !strings.Contains(dataDir, tempHome) { 708 t.Errorf("Data directory should be under temp home, got: %s", dataDir) 709 }
+4 -5
internal/store/database_test.go
··· 8 "runtime" 9 "strings" 10 "testing" 11 ) 12 13 func withTempDirs(t *testing.T) string { 14 t.Helper() 15 - tempDir, err := os.MkdirTemp("", "noteleaf-db-test-*") 16 - if err != nil { 17 - t.Fatalf("Failed to create temp directory: %v", err) 18 - } 19 - t.Cleanup(func() { os.RemoveAll(tempDir) }) 20 21 origConfig, origData := GetConfigDir, GetDataDir 22 GetConfigDir = func() (string, error) { return tempDir, nil }
··· 8 "runtime" 9 "strings" 10 "testing" 11 + 12 + "github.com/stormlightlabs/noteleaf/internal/shared" 13 ) 14 15 func withTempDirs(t *testing.T) string { 16 t.Helper() 17 + tempDir, cleanup := shared.CreateTempDir("noteleaf-db-test-*", t) 18 + t.Cleanup(func() { cleanup() }) 19 20 origConfig, origData := GetConfigDir, GetDataDir 21 GetConfigDir = func() (string, error) { return tempDir, nil }
+2 -1
internal/ui/data_list_tui_test.go
··· 5 "time" 6 7 tea "github.com/charmbracelet/bubbletea" 8 ) 9 10 type mockListModel struct { ··· 67 68 if err := suite.WaitFor(func(m tea.Model) bool { 69 view := m.View() 70 - return !containsString(view, "help") 71 }, 1*time.Second); err != nil { 72 t.Errorf("Help should have been hidden: %v", err) 73 }
··· 5 "time" 6 7 tea "github.com/charmbracelet/bubbletea" 8 + "github.com/stormlightlabs/noteleaf/internal/shared" 9 ) 10 11 type mockListModel struct { ··· 68 69 if err := suite.WaitFor(func(m tea.Model) bool { 70 view := m.View() 71 + return !shared.ContainsString(view, "help") 72 }, 1*time.Second); err != nil { 73 t.Errorf("Help should have been hidden: %v", err) 74 }
-1
internal/ui/project_list_adapter.go
··· 14 GetProjects(ctx context.Context) ([]repo.ProjectSummary, error) 15 } 16 17 - 18 func pluralizeCount(count int) string { 19 if count == 1 { 20 return ""
··· 14 GetProjects(ctx context.Context) ([]repo.ProjectSummary, error) 15 } 16 17 func pluralizeCount(count int) string { 18 if count == 1 { 19 return ""
-1
internal/ui/tag_list_adapter.go
··· 14 GetTags(ctx context.Context) ([]repo.TagSummary, error) 15 } 16 17 - 18 // TagSummaryRecord adapts repo.TagSummary to work with DataTable 19 type TagSummaryRecord struct { 20 summary repo.TagSummary
··· 14 GetTags(ctx context.Context) ([]repo.TagSummary, error) 15 } 16 17 // TagSummaryRecord adapts repo.TagSummary to work with DataTable 18 type TagSummaryRecord struct { 19 summary repo.TagSummary
+2 -1
internal/ui/task_edit_interactive_test.go
··· 6 7 tea "github.com/charmbracelet/bubbletea" 8 "github.com/stormlightlabs/noteleaf/internal/models" 9 ) 10 11 func TestInteractiveTUIBehavior(t *testing.T) { ··· 195 t.Error("View should not be empty") 196 } 197 198 - if !containsString(view, "Test Output") { 199 t.Error("View should contain task description") 200 } 201 })
··· 6 7 tea "github.com/charmbracelet/bubbletea" 8 "github.com/stormlightlabs/noteleaf/internal/models" 9 + "github.com/stormlightlabs/noteleaf/internal/shared" 10 ) 11 12 func TestInteractiveTUIBehavior(t *testing.T) { ··· 196 t.Error("View should not be empty") 197 } 198 199 + if !shared.ContainsString(view, "Test Output") { 200 t.Error("View should contain task description") 201 } 202 })
+4 -16
internal/ui/test_utilities.go
··· 10 11 tea "github.com/charmbracelet/bubbletea" 12 "github.com/stormlightlabs/noteleaf/internal/models" 13 ) 14 15 type AssertionHelpers struct{} ··· 212 func (suite *TUITestSuite) WaitForView(contains string, timeout time.Duration) error { 213 return suite.WaitFor(func(model tea.Model) bool { 214 view := model.View() 215 - return len(view) > 0 && containsString(view, contains) 216 }, timeout) 217 } 218 ··· 314 func (ah *AssertionHelpers) AssertViewContains(t *testing.T, suite *TUITestSuite, expected string, msg string) { 315 t.Helper() 316 view := suite.GetCurrentView() 317 - if !containsString(view, expected) { 318 t.Errorf("View assertion failed: %s\nView content: %s\nExpected to contain: %s", msg, view, expected) 319 } 320 } ··· 322 func (ah *AssertionHelpers) AssertViewNotContains(t *testing.T, suite *TUITestSuite, unexpected string, msg string) { 323 t.Helper() 324 view := suite.GetCurrentView() 325 - if containsString(view, unexpected) { 326 t.Errorf("View assertion failed: %s\nView content: %s\nShould not contain: %s", msg, view, unexpected) 327 } 328 } ··· 335 } 336 337 var Expect = AssertionHelpers{} 338 - 339 - func containsString(haystack, needle string) bool { 340 - if needle == "" { 341 - return true 342 - } 343 - 344 - for i := 0; i <= len(haystack)-len(needle); i++ { 345 - if haystack[i:i+len(needle)] == needle { 346 - return true 347 - } 348 - } 349 - return false 350 - } 351 352 // Test generators for switch case coverage 353 type SwitchCaseTest struct {
··· 10 11 tea "github.com/charmbracelet/bubbletea" 12 "github.com/stormlightlabs/noteleaf/internal/models" 13 + "github.com/stormlightlabs/noteleaf/internal/shared" 14 ) 15 16 type AssertionHelpers struct{} ··· 213 func (suite *TUITestSuite) WaitForView(contains string, timeout time.Duration) error { 214 return suite.WaitFor(func(model tea.Model) bool { 215 view := model.View() 216 + return len(view) > 0 && shared.ContainsString(view, contains) 217 }, timeout) 218 } 219 ··· 315 func (ah *AssertionHelpers) AssertViewContains(t *testing.T, suite *TUITestSuite, expected string, msg string) { 316 t.Helper() 317 view := suite.GetCurrentView() 318 + if !shared.ContainsString(view, expected) { 319 t.Errorf("View assertion failed: %s\nView content: %s\nExpected to contain: %s", msg, view, expected) 320 } 321 } ··· 323 func (ah *AssertionHelpers) AssertViewNotContains(t *testing.T, suite *TUITestSuite, unexpected string, msg string) { 324 t.Helper() 325 view := suite.GetCurrentView() 326 + if shared.ContainsString(view, unexpected) { 327 t.Errorf("View assertion failed: %s\nView content: %s\nShould not contain: %s", msg, view, unexpected) 328 } 329 } ··· 336 } 337 338 var Expect = AssertionHelpers{} 339 340 // Test generators for switch case coverage 341 type SwitchCaseTest struct {