tangled
alpha
login
or
join now
desertthunder.dev
/
noteleaf
cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐
charm
leaflet
readability
golang
29
fork
atom
overview
issues
2
pulls
pipelines
refactor: test helpers & shared utilities
desertthunder.dev
4 months ago
37680959
6b77093b
+1449
-1516
32 changed files
expand all
collapse all
unified
split
internal
articles
parser.go
parser_test.go
handlers
articles_test.go
books_test.go
config_test.go
notes_test.go
seed_test.go
tasks_test.go
test_utilities.go
models
models_test.go
repo
article_repository_test.go
base_media_repository_test.go
book_repository_test.go
find_methods_test.go
movie_repository_test.go
note_repository_test.go
task_repository_test.go
test_utilities.go
time_entry_repository_test.go
tv_repository_test.go
services
http_test.go
shared
shared.go
shared_test.go
test_utilities.go
store
config.go
config_test.go
database_test.go
ui
data_list_tui_test.go
project_list_adapter.go
tag_list_adapter.go
task_edit_interactive_test.go
test_utilities.go
+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
0
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
0
0
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
0
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) {
0
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
0
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
}
0
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
0
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
0
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
0
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
0
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"
0
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"
0
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
)
0
0
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
0
823
func TestBookHandlerWithSuite(t *testing.T) {
0
824
t.Run("Lifecycle", func(t *testing.T) {
825
suite := NewMediaHandlerTestSuite(t)
826
···
828
suite.AssertNoError(err, "NewBookHandler")
829
defer handler.Close()
830
0
831
InputReaderTest(t, handler)
832
})
833
0
834
t.Run("GenericHandlerCreation", func(t *testing.T) {
835
_ = NewHandlerTestSuite(t)
0
0
836
handler := CreateHandler(t, NewBookHandler)
0
0
837
_ = handler
838
})
839
0
840
t.Run("MediaOperations", func(t *testing.T) {
841
suite := NewMediaHandlerTestSuite(t)
842
···
844
suite.AssertNoError(err, "NewBookHandler")
845
defer handler.Close()
846
0
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)
0
854
855
+
suite.AssertNoError(err, "Create test book")
856
suite.TestList(handler, "")
0
0
857
suite.TestUpdateStatus(handler, strconv.FormatInt(id, 10), "reading", true)
0
0
858
suite.TestUpdateStatus(handler, strconv.FormatInt(id, 10), "invalid", false)
0
0
859
suite.TestRemove(handler, strconv.FormatInt(id, 10), true)
860
})
861
0
862
t.Run("GenericLifecycle", func(t *testing.T) {
863
_ = NewHandlerTestSuite(t)
0
0
864
HandlerLifecycleTest(t, NewBookHandler)
865
})
866
}
+239
-261
internal/handlers/config_test.go
···
8
"strings"
9
"testing"
10
0
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
0
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
0
0
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
-
})
0
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
0
133
134
-
err = handler.Set("editor", "emacs")
135
-
if err != nil {
136
-
t.Fatalf("Set failed: %v", err)
137
-
}
0
0
0
0
0
138
139
-
w.Close()
140
-
os.Stdout = oldStdout
0
141
142
-
var buf bytes.Buffer
143
-
io.Copy(&buf, r)
144
-
output := buf.String()
0
145
146
-
if !strings.Contains(output, "Set editor = emacs") {
147
-
t.Errorf("Output should confirm setting, got: %s", output)
148
-
}
0
0
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
-
}
0
0
0
0
0
0
0
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
0
256
257
-
err = handler.Path()
258
-
if err != nil {
259
-
t.Fatalf("Path failed: %v", err)
260
-
}
0
0
0
0
0
0
0
0
261
262
-
w.Close()
263
-
os.Stdout = oldStdout
0
0
264
265
-
var buf bytes.Buffer
266
-
io.Copy(&buf, r)
267
-
output := strings.TrimSpace(buf.String())
0
0
268
269
-
if output != customConfigPath {
270
-
t.Errorf("Expected path '%s', got '%s'", customConfigPath, output)
271
-
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
-
}
0
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()
0
0
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)
0
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)
0
0
0
0
0
0
0
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
0
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
0
49
50
+
var buf bytes.Buffer
51
+
io.Copy(&buf, r)
52
+
output := buf.String()
0
0
0
0
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
+
})
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0
71
72
+
err = handler.Get("editor")
73
+
if err != nil {
74
+
t.Fatalf("Get failed: %v", err)
75
+
}
0
0
0
0
0
76
77
+
w.Close()
78
+
os.Stdout = oldStdout
0
0
0
0
79
80
+
var buf bytes.Buffer
81
+
io.Copy(&buf, r)
82
+
output := buf.String()
0
0
83
84
+
if !strings.Contains(output, "editor = vim") {
85
+
t.Errorf("Output should contain 'editor = vim', got: %s", output)
86
+
}
87
+
})
0
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
0
0
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
0
0
0
131
132
+
var buf bytes.Buffer
133
+
io.Copy(&buf, r)
134
+
output := buf.String()
0
135
136
+
if !strings.Contains(output, "Set editor = emacs") {
137
+
t.Errorf("Output should confirm setting, got: %s", output)
138
+
}
0
0
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
+
})
0
0
0
0
0
0
0
0
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
+
})
0
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
+
}
0
0
0
0
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
+
}
0
0
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
})
0
257
258
+
t.Run("Reset", func(t *testing.T) {
259
+
tempDir, cleanup := shared.CreateTempDir("noteleaf-config-handler-reset-test-*", t)
260
+
defer cleanup()
0
0
0
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
+
}
0
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
0
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
+
}
0
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"
0
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
)
0
0
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
0
0
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"
0
5
"fmt"
6
"io"
0
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
192
// EnvironmentTestHelper provides environment manipulation utilities for testing
0
0
0
193
type EnvironmentTestHelper struct {
194
originalVars map[string]string
195
}
···
252
return tempDir, nil
253
}
254
255
+
// ArticleTestHelper wraps [ArticleHandler] with test-specific functionality
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
389
}
390
391
// MockMediaFetcher provides a test implementation of Fetchable and Searchable interfaces
···
833
},
834
}
835
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+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"
0
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"
0
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"
0
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"
0
0
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"
0
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"
0
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"
0
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"
0
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
173
func AssertCancelledContext(t *testing.T, err error) {
174
+
shared.AssertError(t, err, "Expected error with cancelled context")
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
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"
0
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"
0
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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"
0
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 (
0
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
0
82
if envConfigPath := os.Getenv("NOTELEAF_CONFIG"); envConfigPath != "" {
83
configPath = envConfigPath
0
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) {
0
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"
0
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()
0
0
0
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()
0
0
0
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()
0
0
0
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 {
0
209
t.Fatalf("Failed to write invalid TOML: %v", err)
210
}
211
212
+
if _, err := LoadConfig(); err == nil {
0
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()
0
0
0
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 {
0
234
t.Fatalf("Failed to write config file: %v", err)
235
}
236
237
+
if err := os.Chmod(configPath, 0000); err != nil {
0
238
t.Fatalf("Failed to change file permissions: %v", err)
239
}
240
defer os.Chmod(configPath, 0644)
241
242
+
if _, err := LoadConfig(); err == nil {
0
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()
0
0
0
263
264
_ = filepath.Join(tempDir, ".noteleaf.conf.toml")
265
···
274
}
275
defer func() { GetConfigDir = originalGetConfigDir }()
276
277
+
if _, err := LoadConfig(); err == nil {
0
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 {
0
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()
0
0
0
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 {
0
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 {
0
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()
0
0
0
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()
0
0
0
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
0
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
0
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()
0
0
0
538
539
customConfigPath := filepath.Join(tempDir, "subdir", "config.toml")
540
originalEnv := os.Getenv("NOTELEAF_CONFIG")
···
547
t.Fatalf("SaveConfig failed: %v", err)
548
}
549
0
550
if _, err := os.Stat(customConfigPath); os.IsNotExist(err) {
551
t.Error("Config file should be created at custom NOTELEAF_CONFIG path")
552
}
553
0
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()
0
0
0
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()
0
0
0
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()
0
0
0
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
0
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) {
0
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()
0
0
0
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
0
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) {
0
675
originalEnv := os.Getenv("NOTELEAF_DATA_DIR")
676
os.Unsetenv("NOTELEAF_DATA_DIR")
677
defer os.Setenv("NOTELEAF_DATA_DIR", originalEnv)
678
0
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
0
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"
0
0
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() })
0
0
0
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"
0
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
0
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
0
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"
0
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"
0
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{}
0
0
0
0
0
0
0
0
0
0
0
0
0
339
340
// Test generators for switch case coverage
341
type SwitchCaseTest struct {