summaryrefslogtreecommitdiff
path: root/internal/handlers/budget_logic_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/budget_logic_test.go')
-rw-r--r--internal/handlers/budget_logic_test.go98
1 files changed, 98 insertions, 0 deletions
diff --git a/internal/handlers/budget_logic_test.go b/internal/handlers/budget_logic_test.go
new file mode 100644
index 0000000..1e16595
--- /dev/null
+++ b/internal/handlers/budget_logic_test.go
@@ -0,0 +1,98 @@
+package handlers
+
+import (
+ "testing"
+ "time"
+
+ "task-dashboard/internal/models"
+)
+
+func mustParseInLoc(t *testing.T, layout, value string, loc *time.Location) time.Time {
+ t.Helper()
+ parsed, err := time.ParseInLocation(layout, value, loc)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return parsed
+}
+
+func TestComputeBudgetPeriod_SumsAvailabilityMinusOverlappingEvent(t *testing.T) {
+ loc := time.UTC
+ // A Wednesday: 2026-07-15 is a Wednesday.
+ day := mustParseInLoc(t, "2006-01-02", "2026-07-15", loc)
+ start := day
+ end := day.AddDate(0, 0, 1)
+
+ blocks := []models.AvailabilityBlock{
+ {ID: "b1", Weekday: int(day.Weekday()), StartTime: "18:00", EndTime: "20:00"}, // 120 min
+ }
+ events := []models.CalendarEvent{
+ {ID: "e1", Start: mustParseInLoc(t, "2006-01-02 15:04", "2026-07-15 18:30", loc), End: mustParseInLoc(t, "2006-01-02 15:04", "2026-07-15 19:00", loc)}, // 30 min overlap
+ }
+
+ status := ComputeBudgetPeriod(blocks, events, nil, nil, nil, start, end)
+ if status.AvailableMinutes != 90 {
+ t.Errorf("AvailableMinutes = %d, want 90 (120 - 30 overlap)", status.AvailableMinutes)
+ }
+}
+
+func TestComputeBudgetPeriod_EventFullyOutsideBlockDoesNotReduceIt(t *testing.T) {
+ loc := time.UTC
+ day := mustParseInLoc(t, "2006-01-02", "2026-07-15", loc)
+ start := day
+ end := day.AddDate(0, 0, 1)
+
+ blocks := []models.AvailabilityBlock{
+ {ID: "b1", Weekday: int(day.Weekday()), StartTime: "18:00", EndTime: "20:00"},
+ }
+ events := []models.CalendarEvent{
+ {ID: "e1", Start: mustParseInLoc(t, "2006-01-02 15:04", "2026-07-15 09:00", loc), End: mustParseInLoc(t, "2006-01-02 15:04", "2026-07-15 10:00", loc)},
+ }
+
+ status := ComputeBudgetPeriod(blocks, events, nil, nil, nil, start, end)
+ if status.AvailableMinutes != 120 {
+ t.Errorf("AvailableMinutes = %d, want 120 (event doesn't overlap the block)", status.AvailableMinutes)
+ }
+}
+
+func TestComputeBudgetPeriod_AvailableNeverGoesNegative(t *testing.T) {
+ loc := time.UTC
+ day := mustParseInLoc(t, "2006-01-02", "2026-07-15", loc)
+ start := day
+ end := day.AddDate(0, 0, 1)
+
+ blocks := []models.AvailabilityBlock{
+ {ID: "b1", Weekday: int(day.Weekday()), StartTime: "18:00", EndTime: "20:00"},
+ }
+ events := []models.CalendarEvent{
+ {ID: "e1", Start: mustParseInLoc(t, "2006-01-02 15:04", "2026-07-15 17:00", loc), End: mustParseInLoc(t, "2006-01-02 15:04", "2026-07-15 21:00", loc)},
+ }
+
+ status := ComputeBudgetPeriod(blocks, events, nil, nil, nil, start, end)
+ if status.AvailableMinutes != 0 {
+ t.Errorf("AvailableMinutes = %d, want 0 (event fully covers the block)", status.AvailableMinutes)
+ }
+}
+
+func TestComputeBudgetPeriod_OnlySumsTrackedIncompleteTasksDueInWindow(t *testing.T) {
+ loc := time.UTC
+ start := mustParseInLoc(t, "2006-01-02", "2026-07-15", loc)
+ end := start.AddDate(0, 0, 1)
+ due := mustParseInLoc(t, "2006-01-02 15:04", "2026-07-15 12:00", loc)
+ outsideWindow := end.AddDate(0, 0, 5)
+
+ tasks := []models.Task{
+ {ID: "t-tracked-project", ProjectID: "p1", EstimatedMinutes: 30, DueDate: &due},
+ {ID: "t-tracked-label", Labels: []string{"errands"}, EstimatedMinutes: 20, DueDate: &due},
+ {ID: "t-untracked", ProjectID: "p2", EstimatedMinutes: 999, DueDate: &due},
+ {ID: "t-completed", ProjectID: "p1", EstimatedMinutes: 999, DueDate: &due, Completed: true},
+ {ID: "t-outside-window", ProjectID: "p1", EstimatedMinutes: 999, DueDate: &outsideWindow},
+ }
+ trackedProjects := map[string]bool{"p1": true}
+ trackedLabels := map[string]bool{"errands": true}
+
+ status := ComputeBudgetPeriod(nil, nil, tasks, trackedProjects, trackedLabels, start, end)
+ if status.ScheduledMinutes != 50 {
+ t.Errorf("ScheduledMinutes = %d, want 50 (30 + 20, excluding untracked/completed/out-of-window)", status.ScheduledMinutes)
+ }
+}