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

Configure Feed

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

at main 565 lines 13 kB view raw
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}