Something went wrong. Try again.
cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 🍃
charm leaflet readability golang
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222package models
import ( "encoding/json" "fmt" "testing" "time")
func TestModels(t *testing.T) { t.Run("Model Interface", func(t *testing.T) { now := time.Now() time.Sleep(time.Duration(500) * time.Duration(time.Millisecond)) updated := time.Now()
for i, tc := range []struct { name string model Model unmarshaled Model }{ {name: "Task", model: &Task{ID: 1, Entry: now, Modified: updated}, unmarshaled: &Task{}}, {name: "Movie", model: &Movie{ID: 1, Title: "Test Movie", Year: 2023, Added: now}, unmarshaled: &Movie{}}, {name: "TVShow", model: &TVShow{ID: 1, Title: "Test Show", Added: now}, unmarshaled: &TVShow{}}, {name: "Book", model: &Book{ID: 1, Title: "Test Book", Added: now}, unmarshaled: &Book{}}, {name: "Note", model: &Note{ID: 1, Title: "Test Note", Content: "This is test content", Created: now}, unmarshaled: &Note{}}, {name: "Album", model: &Album{ID: 1, Title: "Test Album", Artist: "Test Artist", Created: now}, unmarshaled: &Album{}}, {name: "TimeEntry", model: &TimeEntry{ID: 1, TaskID: 100, Created: now, Modified: updated}, unmarshaled: &TimeEntry{}}, {name: "Article", model: &Article{ID: 1, Created: now, Modified: updated}, unmarshaled: &Article{}}, } { model := tc.model t.Run(fmt.Sprintf("%v Implementation", tc.name), func(t *testing.T) { model.SetID(int64(i + 1)) if model.GetID() != int64(i+1) { t.Errorf("Model %d: ID not set correctly", i) }
tableName := model.GetTableName() if tableName == "" { t.Errorf("Model %d: table name should not be empty", i) }
now = time.Now() model.SetCreatedAt(now) // NOTE: We don't test exact equality due to potential precision differences if model.GetCreatedAt().IsZero() { t.Errorf("Model %d: created at should not be zero", i) }
updatedAt := time.Now().Add(time.Hour) model.SetUpdatedAt(updatedAt) if !model.GetUpdatedAt().Equal(updatedAt) { t.Errorf("Expected updated at %v, got %v", updatedAt, model.GetUpdatedAt()) }
if model.GetUpdatedAt().IsZero() { t.Errorf("Model %d: updated at should not be zero", i) } model.SetUpdatedAt(now)
t.Run(fmt.Sprintf("%v JSON Marshal/Unmarshal", tc.name), func(t *testing.T) { if data, err := json.Marshal(model); err != nil { t.Fatalf("JSON marshal failed: %v", err) } else { var unmarshaled = tc.unmarshaled if err = json.Unmarshal(data, &unmarshaled); err != nil { t.Fatalf("JSON unmarshal failed: %v", err) }
if unmarshaled.GetID() != model.GetID() { t.Fatalf("IDs should be the same") } } }) }) }
})
t.Run("Task Model", func(t *testing.T) { t.Run("Status Methods", func(t *testing.T) { tc := []struct { status string isCompleted bool isPending bool isDeleted bool }{ {"pending", false, true, false}, {"completed", true, false, false}, {"deleted", false, false, true}, {"unknown", false, false, false}, }
for _, tt := range tc { task := &Task{Status: tt.status}
if task.IsCompleted() != tt.isCompleted { t.Errorf("Status %s: expected IsCompleted %v, got %v", tt.status, tt.isCompleted, task.IsCompleted()) } if task.IsPending() != tt.isPending { t.Errorf("Status %s: expected IsPending %v, got %v", tt.status, tt.isPending, task.IsPending()) } if task.IsDeleted() != tt.isDeleted { t.Errorf("Status %s: expected IsDeleted %v, got %v", tt.status, tt.isDeleted, task.IsDeleted()) } } })
t.Run("New Status Tracking Methods", func(t *testing.T) { tc := []struct { status string isTodo bool isInProgress bool isBlocked bool isDone bool isAbandoned bool }{ {StatusTodo, true, false, false, false, false}, {StatusInProgress, false, true, false, false, false}, {StatusBlocked, false, false, true, false, false}, {StatusDone, false, false, false, true, false}, {StatusAbandoned, false, false, false, false, true}, {"unknown", false, false, false, false, false}, }
for _, tt := range tc { task := &Task{Status: tt.status}
if task.IsTodo() != tt.isTodo { t.Errorf("Status %s: expected IsTodo %v, got %v", tt.status, tt.isTodo, task.IsTodo()) } if task.IsInProgress() != tt.isInProgress { t.Errorf("Status %s: expected IsInProgress %v, got %v", tt.status, tt.isInProgress, task.IsInProgress()) } if task.IsBlocked() != tt.isBlocked { t.Errorf("Status %s: expected IsBlocked %v, got %v", tt.status, tt.isBlocked, task.IsBlocked()) } if task.IsDone() != tt.isDone { t.Errorf("Status %s: expected IsDone %v, got %v", tt.status, tt.isDone, task.IsDone()) } if task.IsAbandoned() != tt.isAbandoned { t.Errorf("Status %s: expected IsAbandoned %v, got %v", tt.status, tt.isAbandoned, task.IsAbandoned()) } } })
t.Run("Status Validation", func(t *testing.T) { validStatuses := []string{ StatusTodo, StatusInProgress, StatusBlocked, StatusDone, StatusAbandoned, StatusPending, StatusCompleted, StatusDeleted, }
for _, status := range validStatuses { task := &Task{Status: status} if !task.IsValidStatus() { t.Errorf("Status %s should be valid", status) } }
invalidStatuses := []string{"unknown", "invalid", ""} for _, status := range invalidStatuses { task := &Task{Status: status} if task.IsValidStatus() { t.Errorf("Status %s should be invalid", status) } } })
t.Run("Priority Methods", func(t *testing.T) { task := &Task{}
if task.HasPriority() { t.Error("Task with empty priority should return false for HasPriority") }
task.Priority = "A" if !task.HasPriority() { t.Error("Task with priority should return true for HasPriority") } })
t.Run("Priority System", func(t *testing.T) { t.Run("Text-based Priority Validation", func(t *testing.T) { validTextPriorities := []string{ PriorityHigh, PriorityMedium, PriorityLow, }
for _, priority := range validTextPriorities { task := &Task{Priority: priority} if !task.IsValidPriority() { t.Errorf("Priority %s should be valid", priority) } } })
t.Run("Numeric Priority Validation", func(t *testing.T) { validNumericPriorities := []string{"1", "2", "3", "4", "5"}
for _, priority := range validNumericPriorities { task := &Task{Priority: priority} if !task.IsValidPriority() { t.Errorf("Numeric priority %s should be valid", priority) } }
invalidNumericPriorities := []string{"0", "6", "10", "-1"} for _, priority := range invalidNumericPriorities { task := &Task{Priority: priority} if task.IsValidPriority() { t.Errorf("Numeric priority %s should be invalid", priority) } } })
t.Run("Legacy A-Z Priority Validation", func(t *testing.T) { validLegacyPriorities := []string{"A", "B", "C", "D", "Z"}
for _, priority := range validLegacyPriorities { task := &Task{Priority: priority} if !task.IsValidPriority() { t.Errorf("Legacy priority %s should be valid", priority) } }
invalidLegacyPriorities := []string{"AA", "a", "1A", ""} for _, priority := range invalidLegacyPriorities { task := &Task{Priority: priority} if priority != "" && task.IsValidPriority() { t.Errorf("Legacy priority %s should be invalid", priority) } } })
t.Run("Empty Priority Validation", func(t *testing.T) { task := &Task{Priority: ""} if !task.IsValidPriority() { t.Error("Empty priority should be valid") } })
t.Run("Priority Weight Calculation", func(t *testing.T) { tc := []struct { priority string weight int }{ {PriorityHigh, 5}, {PriorityMedium, 4}, {PriorityLow, 3}, {"5", 5}, {"4", 4}, {"3", 3}, {"2", 2}, {"1", 1}, {"A", 26}, {"B", 25}, {"C", 24}, {"Z", 1}, {"", 0}, {"invalid", 0}, }
for _, tt := range tc { task := &Task{Priority: tt.priority} weight := task.GetPriorityWeight() if weight != tt.weight { t.Errorf("Priority %s: expected weight %d, got %d", tt.priority, tt.weight, weight) } } })
t.Run("Priority Weight Ordering", func(t *testing.T) { priorities := []string{PriorityHigh, PriorityMedium, PriorityLow} weights := []int{}
for _, priority := range priorities { task := &Task{Priority: priority} weights = append(weights, task.GetPriorityWeight()) }
for i := 1; i < len(weights); i++ { if weights[i-1] <= weights[i] { t.Errorf("Priority weights should be in descending order: %v", weights) } } }) })
t.Run("Tags Marshaling", func(t *testing.T) { task := &Task{}
result, err := task.MarshalTags() if err != nil { t.Fatalf("MarshalTags failed: %v", err) } if result != "" { t.Errorf("Expected empty string for empty tags, got '%s'", result) }
task.Tags = []string{"work", "urgent", "project-x"} result, err = task.MarshalTags() if err != nil { t.Fatalf("MarshalTags failed: %v", err) }
expected := `["work","urgent","project-x"]` if result != expected { t.Errorf("Expected %s, got %s", expected, result) }
newTask := &Task{} err = newTask.UnmarshalTags(result) if err != nil { t.Fatalf("UnmarshalTags failed: %v", err) }
if len(newTask.Tags) != 3 { t.Errorf("Expected 3 tags, got %d", len(newTask.Tags)) } if newTask.Tags[0] != "work" || newTask.Tags[1] != "urgent" || newTask.Tags[2] != "project-x" { t.Errorf("Tags not unmarshaled correctly: %v", newTask.Tags) }
emptyTask := &Task{} err = emptyTask.UnmarshalTags("") if err != nil { t.Fatalf("UnmarshalTags with empty string failed: %v", err) } if emptyTask.Tags != nil { t.Error("Expected nil tags for empty string") } })
t.Run("Annotations Marshaling", func(t *testing.T) { task := &Task{}
result, err := task.MarshalAnnotations() if err != nil { t.Fatalf("MarshalAnnotations failed: %v", err) } if result != "" { t.Errorf("Expected empty string for empty annotations, got '%s'", result) }
task.Annotations = []string{"Note 1", "Note 2", "Important reminder"} result, err = task.MarshalAnnotations() if err != nil { t.Fatalf("MarshalAnnotations failed: %v", err) }
expected := `["Note 1","Note 2","Important reminder"]` if result != expected { t.Errorf("Expected %s, got %s", expected, result) }
newTask := &Task{} err = newTask.UnmarshalAnnotations(result) if err != nil { t.Fatalf("UnmarshalAnnotations failed: %v", err) }
if len(newTask.Annotations) != 3 { t.Errorf("Expected 3 annotations, got %d", len(newTask.Annotations)) } if newTask.Annotations[0] != "Note 1" || newTask.Annotations[1] != "Note 2" || newTask.Annotations[2] != "Important reminder" { t.Errorf("Annotations not unmarshaled correctly: %v", newTask.Annotations) }
emptyTask := &Task{} err = emptyTask.UnmarshalAnnotations("") if err != nil { t.Fatalf("UnmarshalAnnotations with empty string failed: %v", err) } if emptyTask.Annotations != nil { t.Error("Expected nil annotations for empty string") } })
t.Run("IsStarted", func(t *testing.T) { now := time.Now() task := Task{UUID: "123", Description: "demo", Entry: now, Modified: now}
if task.IsStarted() { t.Errorf("expected IsStarted to be false, got true") } task.Start = &now if !task.IsStarted() { t.Errorf("expected IsStarted to be true, got false") } })
t.Run("HasDueDate and IsOverdue", func(t *testing.T) { now := time.Now() past := now.Add(-24 * time.Hour) future := now.Add(24 * time.Hour) task := Task{UUID: "123", Description: "demo", Entry: now, Modified: now}
if task.HasDueDate() { t.Errorf("expected HasDueDate to be false, got true") } task.Due = &future if !task.HasDueDate() { t.Errorf("expected HasDueDate to be true, got false") } task.Due = &past task.Status = string(StatusPending) if !task.IsOverdue(now) { t.Errorf("expected overdue task, got false") } task.Status = string(StatusCompleted) if task.IsOverdue(now) { t.Errorf("expected completed task not to be overdue, got true") } })
t.Run("IsRecurring and IsRecurExpired", func(t *testing.T) { now := time.Now() past := now.Add(-24 * time.Hour) future := now.Add(24 * time.Hour)
task := Task{UUID: "123", Description: "demo", Entry: now, Modified: now} if task.IsRecurring() { t.Errorf("expected IsRecurring to be false, got true") } task.Recur = "FREQ=DAILY" if !task.IsRecurring() { t.Errorf("expected IsRecurring to be true, got false") } if task.IsRecurExpired(now) { t.Errorf("expected IsRecurExpired to be false without Until, got true") } task.Until = &past if !task.IsRecurExpired(now) { t.Errorf("expected IsRecurExpired to be true, got false") } task.Until = &future if task.IsRecurExpired(now) { t.Errorf("expected IsRecurExpired to be false, got true") } })
t.Run("HasDependencies and Blocks", func(t *testing.T) { now := time.Now() task := Task{UUID: "123", Description: "demo", Entry: now, Modified: now} if task.HasDependencies() { t.Errorf("expected HasDependencies to be false, got true") } task.DependsOn = []string{"abc"} if !task.HasDependencies() { t.Errorf("expected HasDependencies to be true, got false") } other := Task{UUID: "abc", DependsOn: []string{"123"}} if !task.Blocks(&other) { t.Errorf("expected task to block other, got false") } other.DependsOn = []string{} if task.Blocks(&other) { t.Errorf("expected task not to block other, got true") } })
t.Run("Urgency", func(t *testing.T) { now := time.Now() past := now.Add(-24 * time.Hour)
task := Task{ UUID: "u1", Description: "urgency test", Priority: "H", Tags: []string{"t1"}, Due: &past, Status: string(StatusPending), Entry: now, Modified: now, } score := task.Urgency(now) if score <= 0 { t.Errorf("expected positive urgency score, got %f", score) } })
})
t.Run("Movie Model", func(t *testing.T) { t.Run("Status Methods", func(t *testing.T) { tc := []struct { status string isWatched bool isQueued bool }{ {"queued", false, true}, {"watched", true, false}, {"removed", false, false}, {"unknown", false, false}, }
for _, tt := range tc { movie := &Movie{Status: tt.status}
if movie.IsWatched() != tt.isWatched { t.Errorf("Status %s: expected IsWatched %v, got %v", tt.status, tt.isWatched, movie.IsWatched()) } if movie.IsQueued() != tt.isQueued { t.Errorf("Status %s: expected IsQueued %v, got %v", tt.status, tt.isQueued, movie.IsQueued()) } } }) })
t.Run("TV Show Model", func(t *testing.T) { t.Run("Status Methods", func(t *testing.T) { tc := []struct { status string isWatching bool isWatched bool isQueued bool }{ {"queued", false, false, true}, {"watching", true, false, false}, {"watched", false, true, false}, {"removed", false, false, false}, {"unknown", false, false, false}, }
for _, tt := range tc { tvShow := &TVShow{Status: tt.status}
if tvShow.IsWatching() != tt.isWatching { t.Errorf("Status %s: expected IsWatching %v, got %v", tt.status, tt.isWatching, tvShow.IsWatching()) } if tvShow.IsWatched() != tt.isWatched { t.Errorf("Status %s: expected IsWatched %v, got %v", tt.status, tt.isWatched, tvShow.IsWatched()) } if tvShow.IsQueued() != tt.isQueued { t.Errorf("Status %s: expected IsQueued %v, got %v", tt.status, tt.isQueued, tvShow.IsQueued()) } } }) })
t.Run("Book Model", func(t *testing.T) { t.Run("Status Methods", func(t *testing.T) { tc := []struct { status string isReading bool isFinished bool isQueued bool }{ {"queued", false, false, true}, {"reading", true, false, false}, {"finished", false, true, false}, {"removed", false, false, false}, {"unknown", false, false, false}, }
for _, tt := range tc { book := &Book{Status: tt.status}
if book.IsReading() != tt.isReading { t.Errorf("Status %s: expected IsReading %v, got %v", tt.status, tt.isReading, book.IsReading()) } if book.IsFinished() != tt.isFinished { t.Errorf("Status %s: expected IsFinished %v, got %v", tt.status, tt.isFinished, book.IsFinished()) } if book.IsQueued() != tt.isQueued { t.Errorf("Status %s: expected IsQueued %v, got %v", tt.status, tt.isQueued, book.IsQueued()) } } })
t.Run("Progress Methods", func(t *testing.T) { book := &Book{Progress: 75}
if book.ProgressPercent() != 75 { t.Errorf("Expected progress 75%%, got %d%%", book.ProgressPercent()) } }) })
t.Run("Note Model", func(t *testing.T) { t.Run("Archive Methods", func(t *testing.T) { note := &Note{Archived: false}
if note.IsArchived() { t.Error("Note should not be archived") }
note.Archived = true if !note.IsArchived() { t.Error("Note should be archived") } })
t.Run("Tags Marshaling", func(t *testing.T) { note := &Note{}
result, err := note.MarshalTags() if err != nil { t.Fatalf("MarshalTags failed: %v", err) } if result != "" { t.Errorf("Expected empty string for empty tags, got '%s'", result) }
note.Tags = []string{"personal", "work", "idea"} result, err = note.MarshalTags() if err != nil { t.Fatalf("MarshalTags failed: %v", err) }
expected := `["personal","work","idea"]` if result != expected { t.Errorf("Expected %s, got %s", expected, result) }
newNote := &Note{} err = newNote.UnmarshalTags(result) if err != nil { t.Fatalf("UnmarshalTags failed: %v", err) }
if len(newNote.Tags) != 3 { t.Errorf("Expected 3 tags, got %d", len(newNote.Tags)) } if newNote.Tags[0] != "personal" || newNote.Tags[1] != "work" || newNote.Tags[2] != "idea" { t.Errorf("Tags not unmarshaled correctly: %v", newNote.Tags) }
emptyNote := &Note{} err = emptyNote.UnmarshalTags("") if err != nil { t.Fatalf("UnmarshalTags with empty string failed: %v", err) } if emptyNote.Tags != nil { t.Error("Expected nil tags for empty string") } })
t.Run("Leaflet Association Methods", func(t *testing.T) { t.Run("has no leaflet association by default", func(t *testing.T) { note := &Note{} if note.HasLeafletAssociation() { t.Error("Note with nil leaflet_rkey should not have association") } })
t.Run("has leaflet association when rkey is set", func(t *testing.T) { rkey := "test-rkey-123" note := &Note{LeafletRKey: &rkey}
if !note.HasLeafletAssociation() { t.Error("Note with leaflet_rkey should have association") } })
t.Run("is not published by default", func(t *testing.T) { note := &Note{IsDraft: true} if note.IsPublished() { t.Error("Draft note should not be published") } })
t.Run("is published when has association and not draft", func(t *testing.T) { rkey := "published-rkey" note := &Note{ LeafletRKey: &rkey, IsDraft: false, } if !note.IsPublished() { t.Error("Note with leaflet association and not draft should be published") } })
t.Run("tracks publication metadata", func(t *testing.T) { rkey := "test-rkey" cid := "test-cid" pubTime := time.Now()
note := &Note{ Title: "Test Note", Content: "Test content", LeafletRKey: &rkey, LeafletCID: &cid, PublishedAt: &pubTime, IsDraft: false, }
if !note.HasLeafletAssociation() { t.Error("Note should have leaflet association") }
if !note.IsPublished() { t.Error("Note should be published") }
if note.LeafletRKey == nil || *note.LeafletRKey != rkey { t.Errorf("Expected rkey %s, got %v", rkey, note.LeafletRKey) }
if note.LeafletCID == nil || *note.LeafletCID != cid { t.Errorf("Expected cid %s, got %v", cid, note.LeafletCID) }
if note.PublishedAt == nil || !note.PublishedAt.Equal(pubTime) { t.Errorf("Expected published_at %v, got %v", pubTime, note.PublishedAt) } })
t.Run("handles draft status", func(t *testing.T) { rkey := "draft-rkey" note := &Note{ Title: "Draft Note", Content: "Draft content", LeafletRKey: &rkey, IsDraft: true, }
if !note.HasLeafletAssociation() { t.Error("Draft should still have leaflet association") }
if note.IsPublished() { t.Error("Draft should not be published") } }) }) })
t.Run("Album Model", func(t *testing.T) { t.Run("Rating Methods", func(t *testing.T) { album := &Album{}
if album.HasRating() { t.Error("Album with zero rating should return false for HasRating") }
if album.IsValidRating() { t.Error("Album with zero rating should return false for IsValidRating") }
album.Rating = 3 if !album.HasRating() { t.Error("Album with rating should return true for HasRating") }
if !album.IsValidRating() { t.Error("Album with valid rating should return true for IsValidRating") }
for _, tc := range []struct { rating int isValid bool }{{0, false}, {1, true}, {3, true}, {5, true}, {6, false}, {-1, false}} { album.Rating = tc.rating if album.IsValidRating() != tc.isValid { t.Errorf("Rating %d: expected IsValidRating %v, got %v", tc.rating, tc.isValid, album.IsValidRating()) } } })
t.Run("Tracks Marshaling", func(t *testing.T) { album := &Album{}
if result, err := album.MarshalTracks(); err != nil { t.Fatalf("MarshalTracks failed: %v", err) } else { if result != "" { t.Errorf("Expected empty string for empty tracks, got '%s'", result) } }
album.Tracks = []string{"Track 1", "Track 2", "Interlude"} result, err := album.MarshalTracks() if err != nil { t.Fatalf("MarshalTracks failed: %v", err) }
if expected := `["Track 1","Track 2","Interlude"]`; result != expected { t.Errorf("Expected %s, got %s", expected, result) }
newAlbum := &Album{} if err = newAlbum.UnmarshalTracks(result); err != nil { t.Fatalf("UnmarshalTracks failed: %v", err) } else { if len(newAlbum.Tracks) != 3 { t.Errorf("Expected 3 tracks, got %d", len(newAlbum.Tracks)) }
if newAlbum.Tracks[0] != "Track 1" || newAlbum.Tracks[1] != "Track 2" || newAlbum.Tracks[2] != "Interlude" { t.Errorf("Tracks not unmarshaled correctly: %v", newAlbum.Tracks) } }
emptyAlbum := &Album{} if err = emptyAlbum.UnmarshalTracks(""); err != nil { t.Fatalf("UnmarshalTracks with empty string failed: %v", err) } else if emptyAlbum.Tracks != nil { t.Error("Expected nil tracks for empty string") } }) })
t.Run("Article Model", func(t *testing.T) { article := Article{URL: "", Author: "", Date: ""} want := false
for _, tc := range []func() bool{article.HasAuthor, article.HasDate, article.IsValidURL} { got := tc() if got != want { t.Errorf("wanted %v, got %v", want, got) } }
article.URL = "http//wikipedia.org" if article.IsValidURL() != want { t.Errorf("%v is invalid but got valid", article.URL) }
article.URL = "http://wikipedia.org" if !article.IsValidURL() { t.Errorf("%v should be valid", article.URL) } })
t.Run("TimeEntry Model", func(t *testing.T) { t.Run("IsActive", func(t *testing.T) { now := time.Now()
t.Run("returns true when EndTime is nil", func(t *testing.T) { te := &TimeEntry{ TaskID: 1, StartTime: now, EndTime: nil, }
if !te.IsActive() { t.Error("TimeEntry with nil EndTime should be active") } })
t.Run("returns false when EndTime is set", func(t *testing.T) { endTime := now.Add(time.Hour) te := &TimeEntry{ TaskID: 1, StartTime: now, EndTime: &endTime, }
if te.IsActive() { t.Error("TimeEntry with EndTime should not be active") } }) })
t.Run("Stop", func(t *testing.T) { startTime := time.Now().Add(-time.Hour) te := &TimeEntry{ TaskID: 1, StartTime: startTime, EndTime: nil, Created: startTime, Modified: startTime, }
if !te.IsActive() { t.Error("TimeEntry should be active before Stop()") }
te.Stop()
if te.IsActive() { t.Error("TimeEntry should not be active after Stop()") }
if te.EndTime == nil { t.Error("EndTime should be set after Stop()") }
if te.EndTime.Before(startTime) { t.Error("EndTime should be after StartTime") }
expectedDuration := int64(te.EndTime.Sub(startTime).Seconds()) if te.DurationSeconds != expectedDuration { t.Errorf("Expected DurationSeconds %d, got %d", expectedDuration, te.DurationSeconds) }
if te.Modified.Before(startTime) { t.Error("Modified time should be updated after Stop()") } })
t.Run("GetDuration", func(t *testing.T) { startTime := time.Now().Add(-time.Hour)
t.Run("returns calculated duration when stopped", func(t *testing.T) { endTime := startTime.Add(30 * time.Minute) te := &TimeEntry{ TaskID: 1, StartTime: startTime, EndTime: &endTime, DurationSeconds: 1800, }
duration := te.GetDuration() expectedDuration := 30 * time.Minute
if duration != expectedDuration { t.Errorf("Expected duration %v, got %v", expectedDuration, duration) } })
t.Run("returns time since start when active", func(t *testing.T) { te := &TimeEntry{ TaskID: 1, StartTime: startTime, EndTime: nil, }
duration := te.GetDuration()
if duration < 59*time.Minute || duration > 61*time.Minute { t.Errorf("Expected duration around 1 hour, got %v", duration) } }) }) })
t.Run("Error Handling", func(t *testing.T) { t.Run("Marshaling Errors", func(t *testing.T) { t.Run("UnmarshalTags handles invalid JSON", func(t *testing.T) { task := &Task{} if err := task.UnmarshalTags(`{"invalid": "json"}`); err == nil { t.Error("Expected error for invalid JSON, got nil") } })
t.Run("UnmarshalAnnotations handles invalid JSON", func(t *testing.T) { task := &Task{} if err := task.UnmarshalAnnotations(`{"invalid": "json"}`); err == nil { t.Error("Expected error for invalid JSON, got nil") } }) }) })
t.Run("Edge Cases", func(t *testing.T) { t.Run("Task with nil slices", func(t *testing.T) { task := &Task{ Tags: nil, Annotations: nil, }
if tagsJSON, err := task.MarshalTags(); err != nil { t.Errorf("MarshalTags with nil slice failed: %v", err) } else if tagsJSON != "" { t.Errorf("Expected empty string for nil tags, got '%s'", tagsJSON) }
if annotationsJSON, err := task.MarshalAnnotations(); err != nil { t.Errorf("MarshalAnnotations with nil slice failed: %v", err) } else if annotationsJSON != "" { t.Errorf("Expected empty string for nil annotations, got '%s'", annotationsJSON) } })
t.Run("Models with zero values", func(t *testing.T) { task := &Task{} movie := &Movie{} tvShow := &TVShow{} book := &Book{} note := &Note{}
if task.IsCompleted() || task.IsPending() || task.IsDeleted() { t.Error("Zero value task should have false status methods") }
if movie.IsWatched() || movie.IsQueued() { t.Error("Zero value movie should have false status methods") }
if tvShow.IsWatching() || tvShow.IsWatched() || tvShow.IsQueued() { t.Error("Zero value TV show should have false status methods") }
if book.IsReading() || book.IsFinished() || book.IsQueued() { t.Error("Zero value book should have false status methods") }
if book.ProgressPercent() != 0 { t.Errorf("Zero value book should have 0%% progress, got %d%%", book.ProgressPercent()) }
if note.IsArchived() { t.Error("Zero value note should not be archived") } }) })
t.Run("Behavior Interfaces", func(t *testing.T) { t.Run("Stateful Interface", func(t *testing.T) { t.Run("Task implements Stateful", func(t *testing.T) { task := &Task{Status: StatusTodo}
if task.GetStatus() != StatusTodo { t.Errorf("Expected status %s, got %s", StatusTodo, task.GetStatus()) }
validStatuses := task.ValidStatuses() if len(validStatuses) == 0 { t.Error("ValidStatuses should not be empty") }
expectedStatuses := []string{StatusTodo, StatusInProgress, StatusBlocked, StatusDone, StatusAbandoned, StatusPending, StatusCompleted, StatusDeleted} if len(validStatuses) != len(expectedStatuses) { t.Errorf("Expected %d valid statuses, got %d", len(expectedStatuses), len(validStatuses)) } })
t.Run("Book implements Stateful", func(t *testing.T) { book := &Book{Status: "reading"}
if book.GetStatus() != "reading" { t.Errorf("Expected status 'reading', got %s", book.GetStatus()) }
validStatuses := book.ValidStatuses() expectedStatuses := []string{"queued", "reading", "finished", "removed"}
if len(validStatuses) != len(expectedStatuses) { t.Errorf("Expected %d valid statuses, got %d", len(expectedStatuses), len(validStatuses)) }
for i, status := range expectedStatuses { if validStatuses[i] != status { t.Errorf("Expected status %s at index %d, got %s", status, i, validStatuses[i]) } } })
t.Run("Movie implements Stateful", func(t *testing.T) { movie := &Movie{Status: "queued"}
if movie.GetStatus() != "queued" { t.Errorf("Expected status 'queued', got %s", movie.GetStatus()) }
validStatuses := movie.ValidStatuses() expectedStatuses := []string{"queued", "watched", "removed"}
if len(validStatuses) != len(expectedStatuses) { t.Errorf("Expected %d valid statuses, got %d", len(expectedStatuses), len(validStatuses)) } })
t.Run("TVShow implements Stateful", func(t *testing.T) { tvShow := &TVShow{Status: "watching"}
if tvShow.GetStatus() != "watching" { t.Errorf("Expected status 'watching', got %s", tvShow.GetStatus()) }
validStatuses := tvShow.ValidStatuses() expectedStatuses := []string{"queued", "watching", "watched", "removed"}
if len(validStatuses) != len(expectedStatuses) { t.Errorf("Expected %d valid statuses, got %d", len(expectedStatuses), len(validStatuses)) } }) })
t.Run("Completable Interface", func(t *testing.T) { t.Run("Book implements Completable", func(t *testing.T) { now := time.Now()
unfinishedBook := &Book{Status: "reading"} if unfinishedBook.IsCompleted() { t.Error("Book with 'reading' status should not be completed") } if unfinishedBook.GetCompletionTime() != nil { t.Error("Unfinished book should have nil completion time") }
finishedBook := &Book{Status: "finished", Finished: &now} if !finishedBook.IsCompleted() { t.Error("Book with 'finished' status should be completed") } if finishedBook.GetCompletionTime() == nil { t.Error("Finished book should have completion time") } if !finishedBook.GetCompletionTime().Equal(now) { t.Errorf("Expected completion time %v, got %v", now, finishedBook.GetCompletionTime()) } })
t.Run("Movie implements Completable", func(t *testing.T) { now := time.Now()
unwatchedMovie := &Movie{Status: "queued"} if unwatchedMovie.IsCompleted() { t.Error("Movie with 'queued' status should not be completed") } if unwatchedMovie.GetCompletionTime() != nil { t.Error("Unwatched movie should have nil completion time") }
watchedMovie := &Movie{Status: "watched", Watched: &now} if !watchedMovie.IsCompleted() { t.Error("Movie with 'watched' status should be completed") } if watchedMovie.GetCompletionTime() == nil { t.Error("Watched movie should have completion time") } if !watchedMovie.GetCompletionTime().Equal(now) { t.Errorf("Expected completion time %v, got %v", now, watchedMovie.GetCompletionTime()) } })
t.Run("TVShow implements Completable", func(t *testing.T) { now := time.Now()
unwatchedShow := &TVShow{Status: "watching"} if unwatchedShow.IsCompleted() { t.Error("TVShow with 'watching' status should not be completed") } if unwatchedShow.GetCompletionTime() != nil { t.Error("Unwatched show should have nil completion time") }
watchedShow := &TVShow{Status: "watched", LastWatched: &now} if !watchedShow.IsCompleted() { t.Error("TVShow with 'watched' status should be completed") } if watchedShow.GetCompletionTime() == nil { t.Error("Watched show should have completion time") } if !watchedShow.GetCompletionTime().Equal(now) { t.Errorf("Expected completion time %v, got %v", now, watchedShow.GetCompletionTime()) } }) })
t.Run("Progressable Interface", func(t *testing.T) { t.Run("Book implements Progressable", func(t *testing.T) { book := &Book{Progress: 50}
if book.GetProgress() != 50 { t.Errorf("Expected progress 50, got %d", book.GetProgress()) } })
t.Run("SetProgress with valid values", func(t *testing.T) { book := &Book{}
if err := book.SetProgress(0); err != nil { t.Errorf("SetProgress(0) should succeed, got error: %v", err) } if book.Progress != 0 { t.Errorf("Expected progress 0, got %d", book.Progress) }
if err := book.SetProgress(100); err != nil { t.Errorf("SetProgress(100) should succeed, got error: %v", err) } if book.Progress != 100 { t.Errorf("Expected progress 100, got %d", book.Progress) }
if err := book.SetProgress(42); err != nil { t.Errorf("SetProgress(42) should succeed, got error: %v", err) } if book.Progress != 42 { t.Errorf("Expected progress 42, got %d", book.Progress) } })
t.Run("SetProgress rejects invalid values", func(t *testing.T) { book := &Book{Progress: 50}
if err := book.SetProgress(-1); err == nil { t.Error("SetProgress(-1) should fail") } else if book.Progress != 50 { t.Error("Progress should not change on validation error") }
if err := book.SetProgress(101); err == nil { t.Error("SetProgress(101) should fail") } else if book.Progress != 50 { t.Error("Progress should not change on validation error") }
if err := book.SetProgress(-100); err == nil { t.Error("SetProgress(-100) should fail") }
if err := book.SetProgress(1000); err == nil { t.Error("SetProgress(1000) should fail") } })
t.Run("SetProgress error messages", func(t *testing.T) { book := &Book{}
err := book.SetProgress(-5) if err == nil { t.Fatal("Expected error for negative progress") } if err.Error() != "progress must be between 0 and 100, got -5" { t.Errorf("Unexpected error message: %s", err.Error()) }
err = book.SetProgress(150) if err == nil { t.Fatal("Expected error for progress > 100") } if err.Error() != "progress must be between 0 and 100, got 150" { t.Errorf("Unexpected error message: %s", err.Error()) } }) }) })}