cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package models
2
3import (
4 "encoding/json"
5 "fmt"
6 "testing"
7 "time"
8)
9
10func TestModels(t *testing.T) {
11 t.Run("Model Interface", func(t *testing.T) {
12 now := time.Now()
13 time.Sleep(time.Duration(500) * time.Duration(time.Millisecond))
14 updated := time.Now()
15
16 for i, tc := range []struct {
17 name string
18 model Model
19 unmarshaled Model
20 }{
21 {name: "Task", model: &Task{ID: 1, Entry: now, Modified: updated}, unmarshaled: &Task{}},
22 {name: "Movie", model: &Movie{ID: 1, Title: "Test Movie", Year: 2023, Added: now}, unmarshaled: &Movie{}},
23 {name: "TVShow", model: &TVShow{ID: 1, Title: "Test Show", Added: now}, unmarshaled: &TVShow{}},
24 {name: "Book", model: &Book{ID: 1, Title: "Test Book", Added: now}, unmarshaled: &Book{}},
25 {name: "Note", model: &Note{ID: 1, Title: "Test Note", Content: "This is test content", Created: now}, unmarshaled: &Note{}},
26 {name: "Album", model: &Album{ID: 1, Title: "Test Album", Artist: "Test Artist", Created: now}, unmarshaled: &Album{}},
27 {name: "TimeEntry", model: &TimeEntry{ID: 1, TaskID: 100, Created: now, Modified: updated}, unmarshaled: &TimeEntry{}},
28 {name: "Article", model: &Article{ID: 1, Created: now, Modified: updated}, unmarshaled: &Article{}},
29 } {
30 model := tc.model
31 t.Run(fmt.Sprintf("%v Implementation", tc.name), func(t *testing.T) {
32 model.SetID(int64(i + 1))
33 if model.GetID() != int64(i+1) {
34 t.Errorf("Model %d: ID not set correctly", i)
35 }
36
37 tableName := model.GetTableName()
38 if tableName == "" {
39 t.Errorf("Model %d: table name should not be empty", i)
40 }
41
42 now = time.Now()
43 model.SetCreatedAt(now)
44 // NOTE: We don't test exact equality due to potential precision differences
45 if model.GetCreatedAt().IsZero() {
46 t.Errorf("Model %d: created at should not be zero", i)
47 }
48
49 updatedAt := time.Now().Add(time.Hour)
50 model.SetUpdatedAt(updatedAt)
51 if !model.GetUpdatedAt().Equal(updatedAt) {
52 t.Errorf("Expected updated at %v, got %v", updatedAt, model.GetUpdatedAt())
53 }
54
55 if model.GetUpdatedAt().IsZero() {
56 t.Errorf("Model %d: updated at should not be zero", i)
57 }
58 model.SetUpdatedAt(now)
59
60 t.Run(fmt.Sprintf("%v JSON Marshal/Unmarshal", tc.name), func(t *testing.T) {
61 if data, err := json.Marshal(model); err != nil {
62 t.Fatalf("JSON marshal failed: %v", err)
63 } else {
64 var unmarshaled = tc.unmarshaled
65 if err = json.Unmarshal(data, &unmarshaled); err != nil {
66 t.Fatalf("JSON unmarshal failed: %v", err)
67 }
68
69 if unmarshaled.GetID() != model.GetID() {
70 t.Fatalf("IDs should be the same")
71 }
72 }
73 })
74 })
75 }
76
77 })
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
85 isDeleted bool
86 }{
87 {"pending", false, true, false},
88 {"completed", true, false, false},
89 {"deleted", false, false, true},
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
113 isBlocked bool
114 isDone bool
115 isAbandoned bool
116 }{
117 {StatusTodo, true, false, false, false, false},
118 {StatusInProgress, false, true, false, false, false},
119 {StatusBlocked, false, false, true, false, false},
120 {StatusDone, false, false, false, true, false},
121 {StatusAbandoned, false, false, false, false, true},
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 })
145
146 t.Run("Status Validation", func(t *testing.T) {
147 validStatuses := []string{
148 StatusTodo, StatusInProgress, StatusBlocked, StatusDone, StatusAbandoned,
149 StatusPending, StatusCompleted, StatusDeleted,
150 }
151
152 for _, status := range validStatuses {
153 task := &Task{Status: status}
154 if !task.IsValidStatus() {
155 t.Errorf("Status %s should be valid", status)
156 }
157 }
158
159 invalidStatuses := []string{"unknown", "invalid", ""}
160 for _, status := range invalidStatuses {
161 task := &Task{Status: status}
162 if task.IsValidStatus() {
163 t.Errorf("Status %s should be invalid", status)
164 }
165 }
166 })
167
168 t.Run("Priority Methods", func(t *testing.T) {
169 task := &Task{}
170
171 if task.HasPriority() {
172 t.Error("Task with empty priority should return false for HasPriority")
173 }
174
175 task.Priority = "A"
176 if !task.HasPriority() {
177 t.Error("Task with priority should return true for HasPriority")
178 }
179 })
180
181 t.Run("Priority System", func(t *testing.T) {
182 t.Run("Text-based Priority Validation", func(t *testing.T) {
183 validTextPriorities := []string{
184 PriorityHigh, PriorityMedium, PriorityLow,
185 }
186
187 for _, priority := range validTextPriorities {
188 task := &Task{Priority: priority}
189 if !task.IsValidPriority() {
190 t.Errorf("Priority %s should be valid", priority)
191 }
192 }
193 })
194
195 t.Run("Numeric Priority Validation", func(t *testing.T) {
196 validNumericPriorities := []string{"1", "2", "3", "4", "5"}
197
198 for _, priority := range validNumericPriorities {
199 task := &Task{Priority: priority}
200 if !task.IsValidPriority() {
201 t.Errorf("Numeric priority %s should be valid", priority)
202 }
203 }
204
205 invalidNumericPriorities := []string{"0", "6", "10", "-1"}
206 for _, priority := range invalidNumericPriorities {
207 task := &Task{Priority: priority}
208 if task.IsValidPriority() {
209 t.Errorf("Numeric priority %s should be invalid", priority)
210 }
211 }
212 })
213
214 t.Run("Legacy A-Z Priority Validation", func(t *testing.T) {
215 validLegacyPriorities := []string{"A", "B", "C", "D", "Z"}
216
217 for _, priority := range validLegacyPriorities {
218 task := &Task{Priority: priority}
219 if !task.IsValidPriority() {
220 t.Errorf("Legacy priority %s should be valid", priority)
221 }
222 }
223
224 invalidLegacyPriorities := []string{"AA", "a", "1A", ""}
225 for _, priority := range invalidLegacyPriorities {
226 task := &Task{Priority: priority}
227 if priority != "" && task.IsValidPriority() {
228 t.Errorf("Legacy priority %s should be invalid", priority)
229 }
230 }
231 })
232
233 t.Run("Empty Priority Validation", func(t *testing.T) {
234 task := &Task{Priority: ""}
235 if !task.IsValidPriority() {
236 t.Error("Empty priority should be valid")
237 }
238 })
239
240 t.Run("Priority Weight Calculation", func(t *testing.T) {
241 testCases := []struct {
242 priority string
243 weight int
244 }{
245 {PriorityHigh, 5},
246 {PriorityMedium, 4},
247 {PriorityLow, 3},
248 {"5", 5},
249 {"4", 4},
250 {"3", 3},
251 {"2", 2},
252 {"1", 1},
253 {"A", 26},
254 {"B", 25},
255 {"C", 24},
256 {"Z", 1},
257 {"", 0},
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 })
269
270 t.Run("Priority Weight Ordering", func(t *testing.T) {
271 priorities := []string{PriorityHigh, PriorityMedium, PriorityLow}
272 weights := []int{}
273
274 for _, priority := range priorities {
275 task := &Task{Priority: priority}
276 weights = append(weights, task.GetPriorityWeight())
277 }
278
279 for i := 1; i < len(weights); i++ {
280 if weights[i-1] <= weights[i] {
281 t.Errorf("Priority weights should be in descending order: %v", weights)
282 }
283 }
284 })
285 })
286
287 t.Run("Tags Marshaling", func(t *testing.T) {
288 task := &Task{}
289
290 result, err := task.MarshalTags()
291 if err != nil {
292 t.Fatalf("MarshalTags failed: %v", err)
293 }
294 if result != "" {
295 t.Errorf("Expected empty string for empty tags, got '%s'", result)
296 }
297
298 task.Tags = []string{"work", "urgent", "project-x"}
299 result, err = task.MarshalTags()
300 if err != nil {
301 t.Fatalf("MarshalTags failed: %v", err)
302 }
303
304 expected := `["work","urgent","project-x"]`
305 if result != expected {
306 t.Errorf("Expected %s, got %s", expected, result)
307 }
308
309 newTask := &Task{}
310 err = newTask.UnmarshalTags(result)
311 if err != nil {
312 t.Fatalf("UnmarshalTags failed: %v", err)
313 }
314
315 if len(newTask.Tags) != 3 {
316 t.Errorf("Expected 3 tags, got %d", len(newTask.Tags))
317 }
318 if newTask.Tags[0] != "work" || newTask.Tags[1] != "urgent" || newTask.Tags[2] != "project-x" {
319 t.Errorf("Tags not unmarshaled correctly: %v", newTask.Tags)
320 }
321
322 emptyTask := &Task{}
323 err = emptyTask.UnmarshalTags("")
324 if err != nil {
325 t.Fatalf("UnmarshalTags with empty string failed: %v", err)
326 }
327 if emptyTask.Tags != nil {
328 t.Error("Expected nil tags for empty string")
329 }
330 })
331
332 t.Run("Annotations Marshaling", func(t *testing.T) {
333 task := &Task{}
334
335 result, err := task.MarshalAnnotations()
336 if err != nil {
337 t.Fatalf("MarshalAnnotations failed: %v", err)
338 }
339 if result != "" {
340 t.Errorf("Expected empty string for empty annotations, got '%s'", result)
341 }
342
343 task.Annotations = []string{"Note 1", "Note 2", "Important reminder"}
344 result, err = task.MarshalAnnotations()
345 if err != nil {
346 t.Fatalf("MarshalAnnotations failed: %v", err)
347 }
348
349 expected := `["Note 1","Note 2","Important reminder"]`
350 if result != expected {
351 t.Errorf("Expected %s, got %s", expected, result)
352 }
353
354 newTask := &Task{}
355 err = newTask.UnmarshalAnnotations(result)
356 if err != nil {
357 t.Fatalf("UnmarshalAnnotations failed: %v", err)
358 }
359
360 if len(newTask.Annotations) != 3 {
361 t.Errorf("Expected 3 annotations, got %d", len(newTask.Annotations))
362 }
363 if newTask.Annotations[0] != "Note 1" || newTask.Annotations[1] != "Note 2" || newTask.Annotations[2] != "Important reminder" {
364 t.Errorf("Annotations not unmarshaled correctly: %v", newTask.Annotations)
365 }
366
367 emptyTask := &Task{}
368 err = emptyTask.UnmarshalAnnotations("")
369 if err != nil {
370 t.Fatalf("UnmarshalAnnotations with empty string failed: %v", err)
371 }
372 if emptyTask.Annotations != nil {
373 t.Error("Expected nil annotations for empty string")
374 }
375 })
376
377 t.Run("IsStarted", func(t *testing.T) {
378 now := time.Now()
379 task := Task{UUID: "123", Description: "demo", Entry: now, Modified: now}
380
381 if task.IsStarted() {
382 t.Errorf("expected IsStarted to be false, got true")
383 }
384 task.Start = &now
385 if !task.IsStarted() {
386 t.Errorf("expected IsStarted to be true, got false")
387 }
388 })
389
390 t.Run("HasDueDate and IsOverdue", func(t *testing.T) {
391 now := time.Now()
392 past := now.Add(-24 * time.Hour)
393 future := now.Add(24 * time.Hour)
394 task := Task{UUID: "123", Description: "demo", Entry: now, Modified: now}
395
396 if task.HasDueDate() {
397 t.Errorf("expected HasDueDate to be false, got true")
398 }
399 task.Due = &future
400 if !task.HasDueDate() {
401 t.Errorf("expected HasDueDate to be true, got false")
402 }
403 task.Due = &past
404 task.Status = string(StatusPending)
405 if !task.IsOverdue(now) {
406 t.Errorf("expected overdue task, got false")
407 }
408 task.Status = string(StatusCompleted)
409 if task.IsOverdue(now) {
410 t.Errorf("expected completed task not to be overdue, got true")
411 }
412 })
413
414 t.Run("IsRecurring and IsRecurExpired", func(t *testing.T) {
415 now := time.Now()
416 past := now.Add(-24 * time.Hour)
417 future := now.Add(24 * time.Hour)
418
419 task := Task{UUID: "123", Description: "demo", Entry: now, Modified: now}
420 if task.IsRecurring() {
421 t.Errorf("expected IsRecurring to be false, got true")
422 }
423 task.Recur = "FREQ=DAILY"
424 if !task.IsRecurring() {
425 t.Errorf("expected IsRecurring to be true, got false")
426 }
427 if task.IsRecurExpired(now) {
428 t.Errorf("expected IsRecurExpired to be false without Until, got true")
429 }
430 task.Until = &past
431 if !task.IsRecurExpired(now) {
432 t.Errorf("expected IsRecurExpired to be true, got false")
433 }
434 task.Until = &future
435 if task.IsRecurExpired(now) {
436 t.Errorf("expected IsRecurExpired to be false, got true")
437 }
438 })
439
440 t.Run("HasDependencies and Blocks", func(t *testing.T) {
441 now := time.Now()
442 task := Task{UUID: "123", Description: "demo", Entry: now, Modified: now}
443 if task.HasDependencies() {
444 t.Errorf("expected HasDependencies to be false, got true")
445 }
446 task.DependsOn = []string{"abc"}
447 if !task.HasDependencies() {
448 t.Errorf("expected HasDependencies to be true, got false")
449 }
450 other := Task{UUID: "abc", DependsOn: []string{"123"}}
451 if !task.Blocks(&other) {
452 t.Errorf("expected task to block other, got false")
453 }
454 other.DependsOn = []string{}
455 if task.Blocks(&other) {
456 t.Errorf("expected task not to block other, got true")
457 }
458 })
459
460 t.Run("Urgency", func(t *testing.T) {
461 now := time.Now()
462 past := now.Add(-24 * time.Hour)
463
464 task := Task{
465 UUID: "u1",
466 Description: "urgency test",
467 Priority: "H",
468 Tags: []string{"t1"},
469 Due: &past,
470 Status: string(StatusPending),
471 Entry: now,
472 Modified: now,
473 }
474 score := task.Urgency(now)
475 if score <= 0 {
476 t.Errorf("expected positive urgency score, got %f", score)
477 }
478 })
479
480 })
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
488 }{
489 {"queued", false, true},
490 {"watched", true, false},
491 {"removed", false, false},
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 })
506 })
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
514 isQueued bool
515 }{
516 {"queued", false, false, true},
517 {"watching", true, false, false},
518 {"watched", false, true, false},
519 {"removed", false, false, false},
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 })
537 })
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
545 isQueued bool
546 }{
547 {"queued", false, false, true},
548 {"reading", true, false, false},
549 {"finished", false, true, false},
550 {"removed", false, false, false},
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 })
568
569 t.Run("Progress Methods", func(t *testing.T) {
570 book := &Book{Progress: 75}
571
572 if book.ProgressPercent() != 75 {
573 t.Errorf("Expected progress 75%%, got %d%%", book.ProgressPercent())
574 }
575 })
576 })
577
578 t.Run("Note Model", func(t *testing.T) {
579 t.Run("Archive Methods", func(t *testing.T) {
580 note := &Note{Archived: false}
581
582 if note.IsArchived() {
583 t.Error("Note should not be archived")
584 }
585
586 note.Archived = true
587 if !note.IsArchived() {
588 t.Error("Note should be archived")
589 }
590 })
591
592 t.Run("Tags Marshaling", func(t *testing.T) {
593 note := &Note{}
594
595 result, err := note.MarshalTags()
596 if err != nil {
597 t.Fatalf("MarshalTags failed: %v", err)
598 }
599 if result != "" {
600 t.Errorf("Expected empty string for empty tags, got '%s'", result)
601 }
602
603 note.Tags = []string{"personal", "work", "idea"}
604 result, err = note.MarshalTags()
605 if err != nil {
606 t.Fatalf("MarshalTags failed: %v", err)
607 }
608
609 expected := `["personal","work","idea"]`
610 if result != expected {
611 t.Errorf("Expected %s, got %s", expected, result)
612 }
613
614 newNote := &Note{}
615 err = newNote.UnmarshalTags(result)
616 if err != nil {
617 t.Fatalf("UnmarshalTags failed: %v", err)
618 }
619
620 if len(newNote.Tags) != 3 {
621 t.Errorf("Expected 3 tags, got %d", len(newNote.Tags))
622 }
623 if newNote.Tags[0] != "personal" || newNote.Tags[1] != "work" || newNote.Tags[2] != "idea" {
624 t.Errorf("Tags not unmarshaled correctly: %v", newNote.Tags)
625 }
626
627 emptyNote := &Note{}
628 err = emptyNote.UnmarshalTags("")
629 if err != nil {
630 t.Fatalf("UnmarshalTags with empty string failed: %v", err)
631 }
632 if emptyNote.Tags != nil {
633 t.Error("Expected nil tags for empty string")
634 }
635 })
636 })
637
638 t.Run("Album Model", func(t *testing.T) {
639 t.Run("Rating Methods", func(t *testing.T) {
640 album := &Album{}
641
642 if album.HasRating() {
643 t.Error("Album with zero rating should return false for HasRating")
644 }
645
646 if album.IsValidRating() {
647 t.Error("Album with zero rating should return false for IsValidRating")
648 }
649
650 album.Rating = 3
651 if !album.HasRating() {
652 t.Error("Album with rating should return true for HasRating")
653 }
654
655 if !album.IsValidRating() {
656 t.Error("Album with valid rating should return true for IsValidRating")
657 }
658
659 for _, tc := range []struct {
660 rating int
661 isValid bool
662 }{{0, false}, {1, true}, {3, true}, {5, true}, {6, false}, {-1, false}} {
663 album.Rating = tc.rating
664 if album.IsValidRating() != tc.isValid {
665 t.Errorf("Rating %d: expected IsValidRating %v, got %v", tc.rating, tc.isValid, album.IsValidRating())
666 }
667 }
668 })
669
670 t.Run("Tracks Marshaling", func(t *testing.T) {
671 album := &Album{}
672
673 if result, err := album.MarshalTracks(); err != nil {
674 t.Fatalf("MarshalTracks failed: %v", err)
675 } else {
676 if result != "" {
677 t.Errorf("Expected empty string for empty tracks, got '%s'", result)
678 }
679 }
680
681 album.Tracks = []string{"Track 1", "Track 2", "Interlude"}
682 result, err := album.MarshalTracks()
683 if err != nil {
684 t.Fatalf("MarshalTracks failed: %v", err)
685 }
686
687 if expected := `["Track 1","Track 2","Interlude"]`; result != expected {
688 t.Errorf("Expected %s, got %s", expected, result)
689 }
690
691 newAlbum := &Album{}
692 if err = newAlbum.UnmarshalTracks(result); err != nil {
693 t.Fatalf("UnmarshalTracks failed: %v", err)
694 } else {
695 if len(newAlbum.Tracks) != 3 {
696 t.Errorf("Expected 3 tracks, got %d", len(newAlbum.Tracks))
697 }
698
699 if newAlbum.Tracks[0] != "Track 1" || newAlbum.Tracks[1] != "Track 2" || newAlbum.Tracks[2] != "Interlude" {
700 t.Errorf("Tracks not unmarshaled correctly: %v", newAlbum.Tracks)
701 }
702 }
703
704 emptyAlbum := &Album{}
705 if err = emptyAlbum.UnmarshalTracks(""); err != nil {
706 t.Fatalf("UnmarshalTracks with empty string failed: %v", err)
707 } else if emptyAlbum.Tracks != nil {
708 t.Error("Expected nil tracks for empty string")
709 }
710 })
711 })
712
713 t.Run("Article Model", func(t *testing.T) {
714 article := Article{URL: "", Author: "", Date: ""}
715 want := false
716
717 for _, tc := range []func() bool{article.HasAuthor, article.HasDate, article.IsValidURL} {
718 got := tc()
719 if got != want {
720 t.Errorf("wanted %v, got %v", want, got)
721 }
722 }
723
724 article.URL = "http//wikipedia.org"
725 if article.IsValidURL() != want {
726 t.Errorf("%v is invalid but got valid", article.URL)
727 }
728
729 article.URL = "http://wikipedia.org"
730 if !article.IsValidURL() {
731 t.Errorf("%v should be valid", article.URL)
732 }
733 })
734
735 t.Run("TimeEntry Model", func(t *testing.T) {
736 t.Run("IsActive", func(t *testing.T) {
737 now := time.Now()
738
739 t.Run("returns true when EndTime is nil", func(t *testing.T) {
740 te := &TimeEntry{
741 TaskID: 1,
742 StartTime: now,
743 EndTime: nil,
744 }
745
746 if !te.IsActive() {
747 t.Error("TimeEntry with nil EndTime should be active")
748 }
749 })
750
751 t.Run("returns false when EndTime is set", func(t *testing.T) {
752 endTime := now.Add(time.Hour)
753 te := &TimeEntry{
754 TaskID: 1,
755 StartTime: now,
756 EndTime: &endTime,
757 }
758
759 if te.IsActive() {
760 t.Error("TimeEntry with EndTime should not be active")
761 }
762 })
763 })
764
765 t.Run("Stop", func(t *testing.T) {
766 startTime := time.Now().Add(-time.Hour)
767 te := &TimeEntry{
768 TaskID: 1,
769 StartTime: startTime,
770 EndTime: nil,
771 Created: startTime,
772 Modified: startTime,
773 }
774
775 if !te.IsActive() {
776 t.Error("TimeEntry should be active before Stop()")
777 }
778
779 te.Stop()
780
781 if te.IsActive() {
782 t.Error("TimeEntry should not be active after Stop()")
783 }
784
785 if te.EndTime == nil {
786 t.Error("EndTime should be set after Stop()")
787 }
788
789 if te.EndTime.Before(startTime) {
790 t.Error("EndTime should be after StartTime")
791 }
792
793 expectedDuration := int64(te.EndTime.Sub(startTime).Seconds())
794 if te.DurationSeconds != expectedDuration {
795 t.Errorf("Expected DurationSeconds %d, got %d", expectedDuration, te.DurationSeconds)
796 }
797
798 if te.Modified.Before(startTime) {
799 t.Error("Modified time should be updated after Stop()")
800 }
801 })
802
803 t.Run("GetDuration", func(t *testing.T) {
804 startTime := time.Now().Add(-time.Hour)
805
806 t.Run("returns calculated duration when stopped", func(t *testing.T) {
807 endTime := startTime.Add(30 * time.Minute)
808 te := &TimeEntry{
809 TaskID: 1,
810 StartTime: startTime,
811 EndTime: &endTime,
812 DurationSeconds: 1800,
813 }
814
815 duration := te.GetDuration()
816 expectedDuration := 30 * time.Minute
817
818 if duration != expectedDuration {
819 t.Errorf("Expected duration %v, got %v", expectedDuration, duration)
820 }
821 })
822
823 t.Run("returns time since start when active", func(t *testing.T) {
824 te := &TimeEntry{
825 TaskID: 1,
826 StartTime: startTime,
827 EndTime: nil,
828 }
829
830 duration := te.GetDuration()
831
832 if duration < 59*time.Minute || duration > 61*time.Minute {
833 t.Errorf("Expected duration around 1 hour, got %v", duration)
834 }
835 })
836 })
837 })
838
839 t.Run("Error Handling", func(t *testing.T) {
840 t.Run("Marshaling Errors", func(t *testing.T) {
841 t.Run("UnmarshalTags handles invalid JSON", func(t *testing.T) {
842 task := &Task{}
843 if err := task.UnmarshalTags(`{"invalid": "json"}`); err == nil {
844 t.Error("Expected error for invalid JSON, got nil")
845 }
846 })
847
848 t.Run("UnmarshalAnnotations handles invalid JSON", func(t *testing.T) {
849 task := &Task{}
850 if err := task.UnmarshalAnnotations(`{"invalid": "json"}`); err == nil {
851 t.Error("Expected error for invalid JSON, got nil")
852 }
853 })
854 })
855 })
856
857 t.Run("Edge Cases", func(t *testing.T) {
858 t.Run("Task with nil slices", func(t *testing.T) {
859 task := &Task{
860 Tags: nil,
861 Annotations: nil,
862 }
863
864 if tagsJSON, err := task.MarshalTags(); err != nil {
865 t.Errorf("MarshalTags with nil slice failed: %v", err)
866 } else if tagsJSON != "" {
867 t.Errorf("Expected empty string for nil tags, got '%s'", tagsJSON)
868 }
869
870 if annotationsJSON, err := task.MarshalAnnotations(); err != nil {
871 t.Errorf("MarshalAnnotations with nil slice failed: %v", err)
872 } else if annotationsJSON != "" {
873 t.Errorf("Expected empty string for nil annotations, got '%s'", annotationsJSON)
874 }
875 })
876
877 t.Run("Models with zero values", func(t *testing.T) {
878 task := &Task{}
879 movie := &Movie{}
880 tvShow := &TVShow{}
881 book := &Book{}
882 note := &Note{}
883
884 if task.IsCompleted() || task.IsPending() || task.IsDeleted() {
885 t.Error("Zero value task should have false status methods")
886 }
887
888 if movie.IsWatched() || movie.IsQueued() {
889 t.Error("Zero value movie should have false status methods")
890 }
891
892 if tvShow.IsWatching() || tvShow.IsWatched() || tvShow.IsQueued() {
893 t.Error("Zero value TV show should have false status methods")
894 }
895
896 if book.IsReading() || book.IsFinished() || book.IsQueued() {
897 t.Error("Zero value book should have false status methods")
898 }
899
900 if book.ProgressPercent() != 0 {
901 t.Errorf("Zero value book should have 0%% progress, got %d%%", book.ProgressPercent())
902 }
903
904 if note.IsArchived() {
905 t.Error("Zero value note should not be archived")
906 }
907 })
908 })
909}