cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 1221 lines 36 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 tc := []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 _, 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 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 _, 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 }) 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 tc := []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 _, 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 }) 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 tc := []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 _, 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 }) 506 }) 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 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 _, 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 }) 537 }) 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 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 _, 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 }) 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 t.Run("Leaflet Association Methods", func(t *testing.T) { 638 t.Run("has no leaflet association by default", func(t *testing.T) { 639 note := &Note{} 640 if note.HasLeafletAssociation() { 641 t.Error("Note with nil leaflet_rkey should not have association") 642 } 643 }) 644 645 t.Run("has leaflet association when rkey is set", func(t *testing.T) { 646 rkey := "test-rkey-123" 647 note := &Note{LeafletRKey: &rkey} 648 649 if !note.HasLeafletAssociation() { 650 t.Error("Note with leaflet_rkey should have association") 651 } 652 }) 653 654 t.Run("is not published by default", func(t *testing.T) { 655 note := &Note{IsDraft: true} 656 if note.IsPublished() { 657 t.Error("Draft note should not be published") 658 } 659 }) 660 661 t.Run("is published when has association and not draft", func(t *testing.T) { 662 rkey := "published-rkey" 663 note := &Note{ 664 LeafletRKey: &rkey, 665 IsDraft: false, 666 } 667 if !note.IsPublished() { 668 t.Error("Note with leaflet association and not draft should be published") 669 } 670 }) 671 672 t.Run("tracks publication metadata", func(t *testing.T) { 673 rkey := "test-rkey" 674 cid := "test-cid" 675 pubTime := time.Now() 676 677 note := &Note{ 678 Title: "Test Note", 679 Content: "Test content", 680 LeafletRKey: &rkey, 681 LeafletCID: &cid, 682 PublishedAt: &pubTime, 683 IsDraft: false, 684 } 685 686 if !note.HasLeafletAssociation() { 687 t.Error("Note should have leaflet association") 688 } 689 690 if !note.IsPublished() { 691 t.Error("Note should be published") 692 } 693 694 if note.LeafletRKey == nil || *note.LeafletRKey != rkey { 695 t.Errorf("Expected rkey %s, got %v", rkey, note.LeafletRKey) 696 } 697 698 if note.LeafletCID == nil || *note.LeafletCID != cid { 699 t.Errorf("Expected cid %s, got %v", cid, note.LeafletCID) 700 } 701 702 if note.PublishedAt == nil || !note.PublishedAt.Equal(pubTime) { 703 t.Errorf("Expected published_at %v, got %v", pubTime, note.PublishedAt) 704 } 705 }) 706 707 t.Run("handles draft status", func(t *testing.T) { 708 rkey := "draft-rkey" 709 note := &Note{ 710 Title: "Draft Note", 711 Content: "Draft content", 712 LeafletRKey: &rkey, 713 IsDraft: true, 714 } 715 716 if !note.HasLeafletAssociation() { 717 t.Error("Draft should still have leaflet association") 718 } 719 720 if note.IsPublished() { 721 t.Error("Draft should not be published") 722 } 723 }) 724 }) 725 }) 726 727 t.Run("Album Model", func(t *testing.T) { 728 t.Run("Rating Methods", func(t *testing.T) { 729 album := &Album{} 730 731 if album.HasRating() { 732 t.Error("Album with zero rating should return false for HasRating") 733 } 734 735 if album.IsValidRating() { 736 t.Error("Album with zero rating should return false for IsValidRating") 737 } 738 739 album.Rating = 3 740 if !album.HasRating() { 741 t.Error("Album with rating should return true for HasRating") 742 } 743 744 if !album.IsValidRating() { 745 t.Error("Album with valid rating should return true for IsValidRating") 746 } 747 748 for _, tc := range []struct { 749 rating int 750 isValid bool 751 }{{0, false}, {1, true}, {3, true}, {5, true}, {6, false}, {-1, false}} { 752 album.Rating = tc.rating 753 if album.IsValidRating() != tc.isValid { 754 t.Errorf("Rating %d: expected IsValidRating %v, got %v", tc.rating, tc.isValid, album.IsValidRating()) 755 } 756 } 757 }) 758 759 t.Run("Tracks Marshaling", func(t *testing.T) { 760 album := &Album{} 761 762 if result, err := album.MarshalTracks(); err != nil { 763 t.Fatalf("MarshalTracks failed: %v", err) 764 } else { 765 if result != "" { 766 t.Errorf("Expected empty string for empty tracks, got '%s'", result) 767 } 768 } 769 770 album.Tracks = []string{"Track 1", "Track 2", "Interlude"} 771 result, err := album.MarshalTracks() 772 if err != nil { 773 t.Fatalf("MarshalTracks failed: %v", err) 774 } 775 776 if expected := `["Track 1","Track 2","Interlude"]`; result != expected { 777 t.Errorf("Expected %s, got %s", expected, result) 778 } 779 780 newAlbum := &Album{} 781 if err = newAlbum.UnmarshalTracks(result); err != nil { 782 t.Fatalf("UnmarshalTracks failed: %v", err) 783 } else { 784 if len(newAlbum.Tracks) != 3 { 785 t.Errorf("Expected 3 tracks, got %d", len(newAlbum.Tracks)) 786 } 787 788 if newAlbum.Tracks[0] != "Track 1" || newAlbum.Tracks[1] != "Track 2" || newAlbum.Tracks[2] != "Interlude" { 789 t.Errorf("Tracks not unmarshaled correctly: %v", newAlbum.Tracks) 790 } 791 } 792 793 emptyAlbum := &Album{} 794 if err = emptyAlbum.UnmarshalTracks(""); err != nil { 795 t.Fatalf("UnmarshalTracks with empty string failed: %v", err) 796 } else if emptyAlbum.Tracks != nil { 797 t.Error("Expected nil tracks for empty string") 798 } 799 }) 800 }) 801 802 t.Run("Article Model", func(t *testing.T) { 803 article := Article{URL: "", Author: "", Date: ""} 804 want := false 805 806 for _, tc := range []func() bool{article.HasAuthor, article.HasDate, article.IsValidURL} { 807 got := tc() 808 if got != want { 809 t.Errorf("wanted %v, got %v", want, got) 810 } 811 } 812 813 article.URL = "http//wikipedia.org" 814 if article.IsValidURL() != want { 815 t.Errorf("%v is invalid but got valid", article.URL) 816 } 817 818 article.URL = "http://wikipedia.org" 819 if !article.IsValidURL() { 820 t.Errorf("%v should be valid", article.URL) 821 } 822 }) 823 824 t.Run("TimeEntry Model", func(t *testing.T) { 825 t.Run("IsActive", func(t *testing.T) { 826 now := time.Now() 827 828 t.Run("returns true when EndTime is nil", func(t *testing.T) { 829 te := &TimeEntry{ 830 TaskID: 1, 831 StartTime: now, 832 EndTime: nil, 833 } 834 835 if !te.IsActive() { 836 t.Error("TimeEntry with nil EndTime should be active") 837 } 838 }) 839 840 t.Run("returns false when EndTime is set", func(t *testing.T) { 841 endTime := now.Add(time.Hour) 842 te := &TimeEntry{ 843 TaskID: 1, 844 StartTime: now, 845 EndTime: &endTime, 846 } 847 848 if te.IsActive() { 849 t.Error("TimeEntry with EndTime should not be active") 850 } 851 }) 852 }) 853 854 t.Run("Stop", func(t *testing.T) { 855 startTime := time.Now().Add(-time.Hour) 856 te := &TimeEntry{ 857 TaskID: 1, 858 StartTime: startTime, 859 EndTime: nil, 860 Created: startTime, 861 Modified: startTime, 862 } 863 864 if !te.IsActive() { 865 t.Error("TimeEntry should be active before Stop()") 866 } 867 868 te.Stop() 869 870 if te.IsActive() { 871 t.Error("TimeEntry should not be active after Stop()") 872 } 873 874 if te.EndTime == nil { 875 t.Error("EndTime should be set after Stop()") 876 } 877 878 if te.EndTime.Before(startTime) { 879 t.Error("EndTime should be after StartTime") 880 } 881 882 expectedDuration := int64(te.EndTime.Sub(startTime).Seconds()) 883 if te.DurationSeconds != expectedDuration { 884 t.Errorf("Expected DurationSeconds %d, got %d", expectedDuration, te.DurationSeconds) 885 } 886 887 if te.Modified.Before(startTime) { 888 t.Error("Modified time should be updated after Stop()") 889 } 890 }) 891 892 t.Run("GetDuration", func(t *testing.T) { 893 startTime := time.Now().Add(-time.Hour) 894 895 t.Run("returns calculated duration when stopped", func(t *testing.T) { 896 endTime := startTime.Add(30 * time.Minute) 897 te := &TimeEntry{ 898 TaskID: 1, 899 StartTime: startTime, 900 EndTime: &endTime, 901 DurationSeconds: 1800, 902 } 903 904 duration := te.GetDuration() 905 expectedDuration := 30 * time.Minute 906 907 if duration != expectedDuration { 908 t.Errorf("Expected duration %v, got %v", expectedDuration, duration) 909 } 910 }) 911 912 t.Run("returns time since start when active", func(t *testing.T) { 913 te := &TimeEntry{ 914 TaskID: 1, 915 StartTime: startTime, 916 EndTime: nil, 917 } 918 919 duration := te.GetDuration() 920 921 if duration < 59*time.Minute || duration > 61*time.Minute { 922 t.Errorf("Expected duration around 1 hour, got %v", duration) 923 } 924 }) 925 }) 926 }) 927 928 t.Run("Error Handling", func(t *testing.T) { 929 t.Run("Marshaling Errors", func(t *testing.T) { 930 t.Run("UnmarshalTags handles invalid JSON", func(t *testing.T) { 931 task := &Task{} 932 if err := task.UnmarshalTags(`{"invalid": "json"}`); err == nil { 933 t.Error("Expected error for invalid JSON, got nil") 934 } 935 }) 936 937 t.Run("UnmarshalAnnotations handles invalid JSON", func(t *testing.T) { 938 task := &Task{} 939 if err := task.UnmarshalAnnotations(`{"invalid": "json"}`); err == nil { 940 t.Error("Expected error for invalid JSON, got nil") 941 } 942 }) 943 }) 944 }) 945 946 t.Run("Edge Cases", func(t *testing.T) { 947 t.Run("Task with nil slices", func(t *testing.T) { 948 task := &Task{ 949 Tags: nil, 950 Annotations: nil, 951 } 952 953 if tagsJSON, err := task.MarshalTags(); err != nil { 954 t.Errorf("MarshalTags with nil slice failed: %v", err) 955 } else if tagsJSON != "" { 956 t.Errorf("Expected empty string for nil tags, got '%s'", tagsJSON) 957 } 958 959 if annotationsJSON, err := task.MarshalAnnotations(); err != nil { 960 t.Errorf("MarshalAnnotations with nil slice failed: %v", err) 961 } else if annotationsJSON != "" { 962 t.Errorf("Expected empty string for nil annotations, got '%s'", annotationsJSON) 963 } 964 }) 965 966 t.Run("Models with zero values", func(t *testing.T) { 967 task := &Task{} 968 movie := &Movie{} 969 tvShow := &TVShow{} 970 book := &Book{} 971 note := &Note{} 972 973 if task.IsCompleted() || task.IsPending() || task.IsDeleted() { 974 t.Error("Zero value task should have false status methods") 975 } 976 977 if movie.IsWatched() || movie.IsQueued() { 978 t.Error("Zero value movie should have false status methods") 979 } 980 981 if tvShow.IsWatching() || tvShow.IsWatched() || tvShow.IsQueued() { 982 t.Error("Zero value TV show should have false status methods") 983 } 984 985 if book.IsReading() || book.IsFinished() || book.IsQueued() { 986 t.Error("Zero value book should have false status methods") 987 } 988 989 if book.ProgressPercent() != 0 { 990 t.Errorf("Zero value book should have 0%% progress, got %d%%", book.ProgressPercent()) 991 } 992 993 if note.IsArchived() { 994 t.Error("Zero value note should not be archived") 995 } 996 }) 997 }) 998 999 t.Run("Behavior Interfaces", func(t *testing.T) { 1000 t.Run("Stateful Interface", func(t *testing.T) { 1001 t.Run("Task implements Stateful", func(t *testing.T) { 1002 task := &Task{Status: StatusTodo} 1003 1004 if task.GetStatus() != StatusTodo { 1005 t.Errorf("Expected status %s, got %s", StatusTodo, task.GetStatus()) 1006 } 1007 1008 validStatuses := task.ValidStatuses() 1009 if len(validStatuses) == 0 { 1010 t.Error("ValidStatuses should not be empty") 1011 } 1012 1013 expectedStatuses := []string{StatusTodo, StatusInProgress, StatusBlocked, StatusDone, StatusAbandoned, StatusPending, StatusCompleted, StatusDeleted} 1014 if len(validStatuses) != len(expectedStatuses) { 1015 t.Errorf("Expected %d valid statuses, got %d", len(expectedStatuses), len(validStatuses)) 1016 } 1017 }) 1018 1019 t.Run("Book implements Stateful", func(t *testing.T) { 1020 book := &Book{Status: "reading"} 1021 1022 if book.GetStatus() != "reading" { 1023 t.Errorf("Expected status 'reading', got %s", book.GetStatus()) 1024 } 1025 1026 validStatuses := book.ValidStatuses() 1027 expectedStatuses := []string{"queued", "reading", "finished", "removed"} 1028 1029 if len(validStatuses) != len(expectedStatuses) { 1030 t.Errorf("Expected %d valid statuses, got %d", len(expectedStatuses), len(validStatuses)) 1031 } 1032 1033 for i, status := range expectedStatuses { 1034 if validStatuses[i] != status { 1035 t.Errorf("Expected status %s at index %d, got %s", status, i, validStatuses[i]) 1036 } 1037 } 1038 }) 1039 1040 t.Run("Movie implements Stateful", func(t *testing.T) { 1041 movie := &Movie{Status: "queued"} 1042 1043 if movie.GetStatus() != "queued" { 1044 t.Errorf("Expected status 'queued', got %s", movie.GetStatus()) 1045 } 1046 1047 validStatuses := movie.ValidStatuses() 1048 expectedStatuses := []string{"queued", "watched", "removed"} 1049 1050 if len(validStatuses) != len(expectedStatuses) { 1051 t.Errorf("Expected %d valid statuses, got %d", len(expectedStatuses), len(validStatuses)) 1052 } 1053 }) 1054 1055 t.Run("TVShow implements Stateful", func(t *testing.T) { 1056 tvShow := &TVShow{Status: "watching"} 1057 1058 if tvShow.GetStatus() != "watching" { 1059 t.Errorf("Expected status 'watching', got %s", tvShow.GetStatus()) 1060 } 1061 1062 validStatuses := tvShow.ValidStatuses() 1063 expectedStatuses := []string{"queued", "watching", "watched", "removed"} 1064 1065 if len(validStatuses) != len(expectedStatuses) { 1066 t.Errorf("Expected %d valid statuses, got %d", len(expectedStatuses), len(validStatuses)) 1067 } 1068 }) 1069 }) 1070 1071 t.Run("Completable Interface", func(t *testing.T) { 1072 t.Run("Book implements Completable", func(t *testing.T) { 1073 now := time.Now() 1074 1075 unfinishedBook := &Book{Status: "reading"} 1076 if unfinishedBook.IsCompleted() { 1077 t.Error("Book with 'reading' status should not be completed") 1078 } 1079 if unfinishedBook.GetCompletionTime() != nil { 1080 t.Error("Unfinished book should have nil completion time") 1081 } 1082 1083 finishedBook := &Book{Status: "finished", Finished: &now} 1084 if !finishedBook.IsCompleted() { 1085 t.Error("Book with 'finished' status should be completed") 1086 } 1087 if finishedBook.GetCompletionTime() == nil { 1088 t.Error("Finished book should have completion time") 1089 } 1090 if !finishedBook.GetCompletionTime().Equal(now) { 1091 t.Errorf("Expected completion time %v, got %v", now, finishedBook.GetCompletionTime()) 1092 } 1093 }) 1094 1095 t.Run("Movie implements Completable", func(t *testing.T) { 1096 now := time.Now() 1097 1098 unwatchedMovie := &Movie{Status: "queued"} 1099 if unwatchedMovie.IsCompleted() { 1100 t.Error("Movie with 'queued' status should not be completed") 1101 } 1102 if unwatchedMovie.GetCompletionTime() != nil { 1103 t.Error("Unwatched movie should have nil completion time") 1104 } 1105 1106 watchedMovie := &Movie{Status: "watched", Watched: &now} 1107 if !watchedMovie.IsCompleted() { 1108 t.Error("Movie with 'watched' status should be completed") 1109 } 1110 if watchedMovie.GetCompletionTime() == nil { 1111 t.Error("Watched movie should have completion time") 1112 } 1113 if !watchedMovie.GetCompletionTime().Equal(now) { 1114 t.Errorf("Expected completion time %v, got %v", now, watchedMovie.GetCompletionTime()) 1115 } 1116 }) 1117 1118 t.Run("TVShow implements Completable", func(t *testing.T) { 1119 now := time.Now() 1120 1121 unwatchedShow := &TVShow{Status: "watching"} 1122 if unwatchedShow.IsCompleted() { 1123 t.Error("TVShow with 'watching' status should not be completed") 1124 } 1125 if unwatchedShow.GetCompletionTime() != nil { 1126 t.Error("Unwatched show should have nil completion time") 1127 } 1128 1129 watchedShow := &TVShow{Status: "watched", LastWatched: &now} 1130 if !watchedShow.IsCompleted() { 1131 t.Error("TVShow with 'watched' status should be completed") 1132 } 1133 if watchedShow.GetCompletionTime() == nil { 1134 t.Error("Watched show should have completion time") 1135 } 1136 if !watchedShow.GetCompletionTime().Equal(now) { 1137 t.Errorf("Expected completion time %v, got %v", now, watchedShow.GetCompletionTime()) 1138 } 1139 }) 1140 }) 1141 1142 t.Run("Progressable Interface", func(t *testing.T) { 1143 t.Run("Book implements Progressable", func(t *testing.T) { 1144 book := &Book{Progress: 50} 1145 1146 if book.GetProgress() != 50 { 1147 t.Errorf("Expected progress 50, got %d", book.GetProgress()) 1148 } 1149 }) 1150 1151 t.Run("SetProgress with valid values", func(t *testing.T) { 1152 book := &Book{} 1153 1154 if err := book.SetProgress(0); err != nil { 1155 t.Errorf("SetProgress(0) should succeed, got error: %v", err) 1156 } 1157 if book.Progress != 0 { 1158 t.Errorf("Expected progress 0, got %d", book.Progress) 1159 } 1160 1161 if err := book.SetProgress(100); err != nil { 1162 t.Errorf("SetProgress(100) should succeed, got error: %v", err) 1163 } 1164 if book.Progress != 100 { 1165 t.Errorf("Expected progress 100, got %d", book.Progress) 1166 } 1167 1168 if err := book.SetProgress(42); err != nil { 1169 t.Errorf("SetProgress(42) should succeed, got error: %v", err) 1170 } 1171 if book.Progress != 42 { 1172 t.Errorf("Expected progress 42, got %d", book.Progress) 1173 } 1174 }) 1175 1176 t.Run("SetProgress rejects invalid values", func(t *testing.T) { 1177 book := &Book{Progress: 50} 1178 1179 if err := book.SetProgress(-1); err == nil { 1180 t.Error("SetProgress(-1) should fail") 1181 } else if book.Progress != 50 { 1182 t.Error("Progress should not change on validation error") 1183 } 1184 1185 if err := book.SetProgress(101); err == nil { 1186 t.Error("SetProgress(101) should fail") 1187 } else if book.Progress != 50 { 1188 t.Error("Progress should not change on validation error") 1189 } 1190 1191 if err := book.SetProgress(-100); err == nil { 1192 t.Error("SetProgress(-100) should fail") 1193 } 1194 1195 if err := book.SetProgress(1000); err == nil { 1196 t.Error("SetProgress(1000) should fail") 1197 } 1198 }) 1199 1200 t.Run("SetProgress error messages", func(t *testing.T) { 1201 book := &Book{} 1202 1203 err := book.SetProgress(-5) 1204 if err == nil { 1205 t.Fatal("Expected error for negative progress") 1206 } 1207 if err.Error() != "progress must be between 0 and 100, got -5" { 1208 t.Errorf("Unexpected error message: %s", err.Error()) 1209 } 1210 1211 err = book.SetProgress(150) 1212 if err == nil { 1213 t.Fatal("Expected error for progress > 100") 1214 } 1215 if err.Error() != "progress must be between 0 and 100, got 150" { 1216 t.Errorf("Unexpected error message: %s", err.Error()) 1217 } 1218 }) 1219 }) 1220 }) 1221}