@@ -0,0 +1,159 @@
+// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+package lunatask_test
+
+import (
+ "testing"
+ "time"
+
+ lunatask "git.secluded.site/go-lunatask"
+)
+
+func TestFilterTasks(t *testing.T) {
+ t.Parallel()
+
+ today := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
+ yesterday := today.Add(-24 * time.Hour)
+ tomorrow := today.Add(24 * time.Hour)
+
+ areaA := "area-a"
+ areaB := "area-b"
+ goalX := "goal-x"
+ goalY := "goal-y"
+
+ tasks := []lunatask.Task{
+ {ID: "1", AreaID: &areaA, GoalID: &goalX, Status: ptr(lunatask.StatusNext)},
+ {ID: "2", AreaID: &areaA, GoalID: &goalY, Status: ptr(lunatask.StatusLater)},
+ {ID: "3", AreaID: &areaB, Status: ptr(lunatask.StatusCompleted), CompletedAt: &yesterday},
+ {ID: "4", AreaID: &areaB, Status: ptr(lunatask.StatusCompleted), CompletedAt: &tomorrow},
+ {ID: "5", Status: ptr(lunatask.StatusInProgress)},
+ }
+
+ completed := ptr(lunatask.StatusCompleted)
+
+ tests := []struct {
+ name string
+ opts *lunatask.TaskFilterOptions
+ wantIDs []string
+ }{
+ {"nil_opts_returns_all", nil, []string{"1", "2", "3", "4", "5"}},
+ {"empty_opts_excludes_old_completed", &lunatask.TaskFilterOptions{Today: today}, []string{"1", "2", "4", "5"}},
+ {"include_completed", &lunatask.TaskFilterOptions{IncludeCompleted: true}, []string{"1", "2", "3", "4", "5"}},
+ {"filter_by_area", &lunatask.TaskFilterOptions{AreaID: &areaA, IncludeCompleted: true}, []string{"1", "2"}},
+ {"filter_by_goal", &lunatask.TaskFilterOptions{GoalID: &goalX, IncludeCompleted: true}, []string{"1"}},
+ {"filter_by_status", &lunatask.TaskFilterOptions{Status: completed, IncludeCompleted: true}, []string{"3", "4"}},
+ {"filter_by_status_includes_old", &lunatask.TaskFilterOptions{Status: completed, Today: today}, []string{"3", "4"}},
+ {"combined_area_and_status", &lunatask.TaskFilterOptions{AreaID: &areaB, Status: completed}, []string{"3", "4"}},
+ {"no_matches", &lunatask.TaskFilterOptions{AreaID: ptr("nonexistent"), IncludeCompleted: true}, []string{}},
+ }
+
+ for _, testCase := range tests {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+ assertFilterResult(t, tasks, testCase.opts, testCase.wantIDs)
+ })
+ }
+}
+
+func TestIsOldCompleted(t *testing.T) {
+ t.Parallel()
+
+ today := time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
+ yesterday := today.Add(-24 * time.Hour)
+ tomorrow := today.Add(24 * time.Hour)
+ completed := ptr(lunatask.StatusCompleted)
+
+ tests := []struct {
+ name string
+ task lunatask.Task
+ today time.Time
+ expect bool
+ }{
+ {"not_completed", lunatask.Task{Status: ptr(lunatask.StatusNext)}, today, false},
+ {"nil_status", lunatask.Task{}, today, false},
+ {"completed_yesterday", lunatask.Task{Status: completed, CompletedAt: &yesterday}, today, true},
+ {"completed_tomorrow", lunatask.Task{Status: completed, CompletedAt: &tomorrow}, today, false},
+ {"completed_nil_timestamp", lunatask.Task{Status: completed}, today, true},
+ {"completed_exactly_today", lunatask.Task{Status: completed, CompletedAt: &today}, today, false},
+ }
+
+ for _, testCase := range tests {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+
+ got := lunatask.IsOldCompleted(testCase.task, testCase.today)
+ if got != testCase.expect {
+ t.Errorf("IsOldCompleted() = %v, want %v", got, testCase.expect)
+ }
+ })
+ }
+}
+
+func TestFilterTasks_NilTaskFields(t *testing.T) {
+ t.Parallel()
+
+ areaA := "area-a"
+ goalX := "goal-x"
+
+ tasks := []lunatask.Task{
+ {ID: "1"},
+ {ID: "2", AreaID: &areaA},
+ {ID: "3", GoalID: &goalX},
+ }
+
+ tests := []struct {
+ name string
+ opts *lunatask.TaskFilterOptions
+ wantIDs []string
+ }{
+ {
+ "area_filter_skips_nil_area",
+ &lunatask.TaskFilterOptions{AreaID: &areaA, IncludeCompleted: true},
+ []string{"2"},
+ },
+ {
+ "goal_filter_skips_nil_goal",
+ &lunatask.TaskFilterOptions{GoalID: &goalX, IncludeCompleted: true},
+ []string{"3"},
+ },
+ {
+ "status_filter_skips_nil_status",
+ &lunatask.TaskFilterOptions{Status: ptr(lunatask.StatusNext), IncludeCompleted: true},
+ []string{},
+ },
+ }
+
+ for _, testCase := range tests {
+ t.Run(testCase.name, func(t *testing.T) {
+ t.Parallel()
+ assertFilterResult(t, tasks, testCase.opts, testCase.wantIDs)
+ })
+ }
+}
+
+func assertFilterResult(t *testing.T, tasks []lunatask.Task, opts *lunatask.TaskFilterOptions, wantIDs []string) {
+ t.Helper()
+
+ got := lunatask.FilterTasks(tasks, opts)
+ gotIDs := make([]string, len(got))
+
+ for i, task := range got {
+ gotIDs[i] = task.ID
+ }
+
+ if len(gotIDs) != len(wantIDs) {
+ t.Errorf("FilterTasks() got %v, want %v", gotIDs, wantIDs)
+
+ return
+ }
+
+ for i, id := range gotIDs {
+ if id != wantIDs[i] {
+ t.Errorf("FilterTasks() got %v, want %v", gotIDs, wantIDs)
+
+ return
+ }
+ }
+}