cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package repo
2
3import (
4 "context"
5 "database/sql"
6 "fmt"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/google/uuid"
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
19var fake = faker.New()
20
21// CreateTestDB creates an in-memory SQLite database with the full schema for testing
22func CreateTestDB(t *testing.T) *sql.DB {
23 t.Helper()
24 db, err := sql.Open("sqlite3", ":memory:")
25 if err != nil {
26 t.Fatalf("Failed to create in-memory database: %v", err)
27 }
28
29 if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
30 t.Fatalf("Failed to enable foreign keys: %v", err)
31 }
32
33 mr := store.NewMigrationRunner(&store.Database{DB: db})
34 if err := mr.RunMigrations(); err != nil {
35 t.Errorf("failed to run migrations %v", err)
36 }
37
38 t.Cleanup(func() {
39 db.Close()
40 })
41
42 return db
43}
44
45func CreateSampleTask() *models.Task {
46 return &models.Task{
47 UUID: uuid.New().String(),
48 Description: "Test Task",
49 Status: "pending",
50 Priority: "medium",
51 Project: "test-project",
52 Context: "test-context",
53 Tags: []string{"test", "sample"},
54 Entry: time.Now(),
55 Modified: time.Now(),
56 }
57}
58
59func CreateSampleBook() *models.Book {
60 return &models.Book{
61 Title: "Test Book",
62 Author: "Test Author",
63 Status: "queued",
64 Progress: 0,
65 Pages: 300,
66 Rating: 4.5,
67 Notes: "Great book!",
68 Added: time.Now(),
69 }
70}
71
72func CreateSampleMovie() *models.Movie {
73 return &models.Movie{
74 Title: "Test Movie",
75 Year: 2023,
76 Status: "queued",
77 Rating: 8.5,
78 Notes: "Excellent film",
79 Added: time.Now(),
80 }
81}
82
83func CreateSampleTVShow() *models.TVShow {
84 return &models.TVShow{
85 Title: "Test TV Show",
86 Season: 1,
87 Episode: 1,
88 Status: "queued",
89 Rating: 9.0,
90 Notes: "Amazing series",
91 Added: time.Now(),
92 }
93}
94
95func CreateSampleNote() *models.Note {
96 return &models.Note{
97 Title: "Test Note",
98 Content: "This is a test note content",
99 Tags: []string{"test", "sample"},
100 Archived: false,
101 Created: time.Now(),
102 Modified: time.Now(),
103 }
104}
105
106func CreateSampleTimeEntry(taskID int64) *models.TimeEntry {
107 startTime := time.Now().Add(-time.Hour)
108 return &models.TimeEntry{
109 TaskID: taskID,
110 StartTime: startTime,
111 EndTime: nil,
112 DurationSeconds: 0,
113 Created: startTime,
114 Modified: startTime,
115 }
116}
117
118func CreateSampleArticle() *models.Article {
119 return &models.Article{
120 URL: "https://example.com/test-article",
121 Title: "Test Article",
122 Author: "Test Author",
123 Date: "2024-01-01",
124 MarkdownPath: "/path/test-article.md",
125 HTMLPath: "/path/test-article.html",
126 Created: time.Now(),
127 Modified: time.Now(),
128 }
129}
130
131func fakeHTMLFile(f faker.Faker) string {
132 original := f.File().AbsoluteFilePath(2)
133 split := strings.Split(original, ".")
134 split[len(split)-1] = "html"
135
136 return strings.Join(split, ".")
137}
138
139func fakeMDFile(f faker.Faker) string {
140 original := f.File().AbsoluteFilePath(2)
141 split := strings.Split(original, ".")
142 split[len(split)-1] = "md"
143
144 return strings.Join(split, ".")
145}
146
147func FakeTime(f faker.Faker) time.Time {
148 return f.Time().Time(time.Now())
149}
150
151func CreateFakeArticle() *models.Article {
152 return &models.Article{
153 URL: fake.Internet().URL(),
154 Title: strings.Join(fake.Lorem().Words(3), " "),
155 Author: fmt.Sprintf("%v %v", fake.Person().FirstName(), fake.Person().LastName()),
156 Date: fake.Time().Time(time.Now()).Format("2006-01-02"),
157 MarkdownPath: fakeMDFile(fake),
158 HTMLPath: fakeHTMLFile(fake),
159 Created: time.Now(),
160 Modified: time.Now(),
161 }
162}
163
164func CreateFakeArticles(count int) []*models.Article {
165 articles := make([]*models.Article, count)
166 for i := range count {
167 articles[i] = CreateFakeArticle()
168 }
169
170 return articles
171}
172
173func AssertCancelledContext(t *testing.T, err error) {
174 shared.AssertError(t, err, "Expected error with cancelled context")
175}
176
177// NewCanceledContext returns a pre-canceled context for testing error conditions
178func NewCanceledContext() context.Context {
179 ctx, cancel := context.WithCancel(context.Background())
180 cancel()
181 return ctx
182}
183
184// TaskBuilder provides a fluent interface for building test tasks
185type TaskBuilder struct {
186 task *models.Task
187}
188
189// NewTaskBuilder creates a new TaskBuilder with sensible defaults
190func NewTaskBuilder() *TaskBuilder {
191 return &TaskBuilder{
192 task: &models.Task{
193 UUID: uuid.New().String(),
194 Status: "pending",
195 Entry: time.Now(),
196 Modified: time.Now(),
197 },
198 }
199}
200
201func (b *TaskBuilder) WithUUID(uuid string) *TaskBuilder {
202 b.task.UUID = uuid
203 return b
204}
205
206func (b *TaskBuilder) WithDescription(desc string) *TaskBuilder {
207 b.task.Description = desc
208 return b
209}
210
211func (b *TaskBuilder) WithStatus(status string) *TaskBuilder {
212 b.task.Status = status
213 return b
214}
215
216func (b *TaskBuilder) WithPriority(priority string) *TaskBuilder {
217 b.task.Priority = priority
218 return b
219}
220
221func (b *TaskBuilder) WithProject(project string) *TaskBuilder {
222 b.task.Project = project
223 return b
224}
225
226func (b *TaskBuilder) WithContext(ctx string) *TaskBuilder {
227 b.task.Context = ctx
228 return b
229}
230
231func (b *TaskBuilder) WithTags(tags []string) *TaskBuilder {
232 b.task.Tags = tags
233 return b
234}
235
236func (b *TaskBuilder) WithDue(due time.Time) *TaskBuilder {
237 b.task.Due = &due
238 return b
239}
240
241func (b *TaskBuilder) WithEnd(end time.Time) *TaskBuilder {
242 b.task.End = &end
243 return b
244}
245
246func (b *TaskBuilder) WithRecur(recur string) *TaskBuilder {
247 b.task.Recur = models.RRule(recur)
248 return b
249}
250
251func (b *TaskBuilder) WithDependsOn(deps []string) *TaskBuilder {
252 b.task.DependsOn = deps
253 return b
254}
255
256func (b *TaskBuilder) Build() *models.Task {
257 return b.task
258}
259
260// BookBuilder provides a fluent interface for building test books
261type BookBuilder struct {
262 book *models.Book
263}
264
265// NewBookBuilder creates a new BookBuilder with sensible defaults
266func NewBookBuilder() *BookBuilder {
267 return &BookBuilder{
268 book: &models.Book{
269 Status: "queued",
270 Progress: 0,
271 Added: time.Now(),
272 },
273 }
274}
275
276func (b *BookBuilder) WithTitle(title string) *BookBuilder {
277 b.book.Title = title
278 return b
279}
280
281func (b *BookBuilder) WithAuthor(author string) *BookBuilder {
282 b.book.Author = author
283 return b
284}
285
286func (b *BookBuilder) WithStatus(status string) *BookBuilder {
287 b.book.Status = status
288 return b
289}
290
291func (b *BookBuilder) WithProgress(progress int) *BookBuilder {
292 b.book.Progress = progress
293 return b
294}
295
296func (b *BookBuilder) WithPages(pages int) *BookBuilder {
297 b.book.Pages = pages
298 return b
299}
300
301func (b *BookBuilder) WithRating(rating float64) *BookBuilder {
302 b.book.Rating = rating
303 return b
304}
305
306func (b *BookBuilder) WithNotes(notes string) *BookBuilder {
307 b.book.Notes = notes
308 return b
309}
310
311func (b *BookBuilder) WithStarted(started time.Time) *BookBuilder {
312 b.book.Started = &started
313 return b
314}
315
316func (b *BookBuilder) WithFinished(finished time.Time) *BookBuilder {
317 b.book.Finished = &finished
318 return b
319}
320
321func (b *BookBuilder) Build() *models.Book {
322 return b.book
323}
324
325// MovieBuilder provides a fluent interface for building test movies
326type MovieBuilder struct {
327 movie *models.Movie
328}
329
330// NewMovieBuilder creates a new MovieBuilder with sensible defaults
331func NewMovieBuilder() *MovieBuilder {
332 return &MovieBuilder{
333 movie: &models.Movie{
334 Status: "queued",
335 Added: time.Now(),
336 },
337 }
338}
339
340func (b *MovieBuilder) WithTitle(title string) *MovieBuilder {
341 b.movie.Title = title
342 return b
343}
344
345func (b *MovieBuilder) WithYear(year int) *MovieBuilder {
346 b.movie.Year = year
347 return b
348}
349
350func (b *MovieBuilder) WithStatus(status string) *MovieBuilder {
351 b.movie.Status = status
352 return b
353}
354
355func (b *MovieBuilder) WithRating(rating float64) *MovieBuilder {
356 b.movie.Rating = rating
357 return b
358}
359
360func (b *MovieBuilder) WithNotes(notes string) *MovieBuilder {
361 b.movie.Notes = notes
362 return b
363}
364
365func (b *MovieBuilder) WithWatched(watched time.Time) *MovieBuilder {
366 b.movie.Watched = &watched
367 return b
368}
369
370func (b *MovieBuilder) Build() *models.Movie {
371 return b.movie
372}
373
374// TVShowBuilder provides a fluent interface for building test TV shows
375type TVShowBuilder struct {
376 show *models.TVShow
377}
378
379// NewTVShowBuilder creates a new TVShowBuilder with sensible defaults
380func NewTVShowBuilder() *TVShowBuilder {
381 return &TVShowBuilder{
382 show: &models.TVShow{
383 Status: "queued",
384 Season: 1,
385 Episode: 1,
386 Added: time.Now(),
387 },
388 }
389}
390
391func (b *TVShowBuilder) WithTitle(title string) *TVShowBuilder {
392 b.show.Title = title
393 return b
394}
395
396func (b *TVShowBuilder) WithSeason(season int) *TVShowBuilder {
397 b.show.Season = season
398 return b
399}
400
401func (b *TVShowBuilder) WithEpisode(episode int) *TVShowBuilder {
402 b.show.Episode = episode
403 return b
404}
405
406func (b *TVShowBuilder) WithStatus(status string) *TVShowBuilder {
407 b.show.Status = status
408 return b
409}
410
411func (b *TVShowBuilder) WithRating(rating float64) *TVShowBuilder {
412 b.show.Rating = rating
413 return b
414}
415
416func (b *TVShowBuilder) WithNotes(notes string) *TVShowBuilder {
417 b.show.Notes = notes
418 return b
419}
420
421func (b *TVShowBuilder) WithLastWatched(lastWatched time.Time) *TVShowBuilder {
422 b.show.LastWatched = &lastWatched
423 return b
424}
425
426func (b *TVShowBuilder) Build() *models.TVShow {
427 return b.show
428}
429
430// NoteBuilder provides a fluent interface for building test notes
431type NoteBuilder struct {
432 note *models.Note
433}
434
435// NewNoteBuilder creates a new NoteBuilder with sensible defaults
436func NewNoteBuilder() *NoteBuilder {
437 return &NoteBuilder{
438 note: &models.Note{
439 Archived: false,
440 Created: time.Now(),
441 Modified: time.Now(),
442 },
443 }
444}
445
446func (b *NoteBuilder) WithTitle(title string) *NoteBuilder {
447 b.note.Title = title
448 return b
449}
450
451func (b *NoteBuilder) WithContent(content string) *NoteBuilder {
452 b.note.Content = content
453 return b
454}
455
456func (b *NoteBuilder) WithTags(tags []string) *NoteBuilder {
457 b.note.Tags = tags
458 return b
459}
460
461func (b *NoteBuilder) WithArchived(archived bool) *NoteBuilder {
462 b.note.Archived = archived
463 return b
464}
465
466func (b *NoteBuilder) WithFilePath(filePath string) *NoteBuilder {
467 b.note.FilePath = filePath
468 return b
469}
470
471func (b *NoteBuilder) Build() *models.Note {
472 return b.note
473}
474
475// SetupTestData creates sample data in the database and returns the repositories
476func SetupTestData(t *testing.T, db *sql.DB) *Repositories {
477 ctx := context.Background()
478 repos := NewRepositories(db)
479
480 // Create sample tasks
481 task1 := CreateSampleTask()
482 task1.Description = "Sample Task 1"
483 task1.Status = "pending"
484 task1.Priority = "high"
485
486 task2 := CreateSampleTask()
487 task2.Description = "Sample Task 2"
488 task2.Status = "completed"
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()
500 book1.Title = "Sample Book 1"
501 book1.Status = "reading"
502
503 book2 := CreateSampleBook()
504 book2.Title = "Sample Book 2"
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()
516 movie1.Title = "Sample Movie 1"
517 movie1.Status = "queued"
518
519 movie2 := CreateSampleMovie()
520 movie2.Title = "Sample Movie 2"
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()
532 tv1.Title = "Sample TV Show 1"
533 tv1.Status = "queued"
534
535 tv2 := CreateSampleTVShow()
536 tv2.Title = "Sample TV Show 2"
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()
548 note1.Title = "Sample Note 1"
549 note1.Content = "Content for note 1"
550
551 note2 := CreateSampleNote()
552 note2.Title = "Sample Note 2"
553 note2.Content = "Content for note 2"
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
565}