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 7452aaa4e1d32eaa8a95e2413dcd589b4c807007 909 lines 26 kB view raw
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}