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 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}