1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package task_test
6
7import (
8 "context"
9 "errors"
10 "testing"
11 "time"
12
13 "git.secluded.site/np/internal/task"
14 "git.secluded.site/np/internal/testutil"
15)
16
17func TestStoreCreateAndList(t *testing.T) {
18 ctx := context.Background()
19 db := testutil.OpenDB(t)
20
21 clock := &testutil.SequenceClock{
22 Times: []time.Time{
23 time.Date(2025, time.January, 1, 9, 0, 0, 0, time.FixedZone("A", 3600)),
24 time.Date(2025, time.January, 1, 9, 5, 0, 0, time.FixedZone("B", -3600)),
25 },
26 }
27
28 store := task.NewStore(db, clock)
29 sid := "sid-create"
30
31 first, err := store.Create(ctx, sid, task.CreateParams{
32 Title: " First Task ",
33 Description: "first description",
34 CreatedSeq: 2,
35 })
36 if err != nil {
37 t.Fatalf("Create first: %v", err)
38 }
39
40 if first.Title != "First Task" {
41 t.Fatalf("Title not trimmed: %q", first.Title)
42 }
43 if first.Status != task.StatusPending {
44 t.Fatalf("Expected default status pending, got %s", first.Status)
45 }
46 if want := clock.Times[0].UTC(); !first.CreatedAt.Equal(want) || !first.UpdatedAt.Equal(want) {
47 t.Fatalf("Expected timestamps %v, got created %v updated %v", want, first.CreatedAt, first.UpdatedAt)
48 }
49 if want := task.GenerateID(sid, "First Task", "first description"); first.ID != want {
50 t.Fatalf("GenerateID mismatch: want %q got %q", want, first.ID)
51 }
52
53 second, err := store.Create(ctx, sid, task.CreateParams{
54 Title: "Second Task",
55 Description: "second description",
56 CreatedSeq: 1,
57 Status: task.StatusInProgress,
58 })
59 if err != nil {
60 t.Fatalf("Create second: %v", err)
61 }
62 if second.Status != task.StatusInProgress {
63 t.Fatalf("Explicit status not applied")
64 }
65 if want := clock.Times[1].UTC(); !second.CreatedAt.Equal(want) {
66 t.Fatalf("Second CreatedAt mismatch: want %v got %v", want, second.CreatedAt)
67 }
68
69 all, err := store.List(ctx, sid)
70 if err != nil {
71 t.Fatalf("List: %v", err)
72 }
73 if len(all) != 2 {
74 t.Fatalf("Expected 2 tasks, got %d", len(all))
75 }
76 if all[0].ID != second.ID || all[1].ID != first.ID {
77 t.Fatalf("Tasks not sorted by CreatedSeq: %+v", all)
78 }
79
80 pending, err := store.ListByStatus(ctx, sid, task.StatusPending)
81 if err != nil {
82 t.Fatalf("ListByStatus pending: %v", err)
83 }
84 if len(pending) != 1 || pending[0].ID != first.ID {
85 t.Fatalf("Pending list mismatch: %+v", pending)
86 }
87
88 byProgress, err := store.ListByStatus(ctx, sid, task.StatusInProgress)
89 if err != nil {
90 t.Fatalf("ListByStatus in_progress: %v", err)
91 }
92 if len(byProgress) != 1 || byProgress[0].ID != second.ID {
93 t.Fatalf("In-progress list mismatch: %+v", byProgress)
94 }
95
96 exists, err := store.Exists(ctx, sid, first.ID)
97 if err != nil {
98 t.Fatalf("Exists: %v", err)
99 }
100 if !exists {
101 t.Fatalf("Expected first task to exist")
102 }
103 exists, err = store.Exists(ctx, sid, "missing")
104 if err != nil {
105 t.Fatalf("Exists missing: %v", err)
106 }
107 if exists {
108 t.Fatalf("Expected missing task to be false")
109 }
110}
111
112func TestStoreUpdateAndStatusIndex(t *testing.T) {
113 ctx := context.Background()
114 db := testutil.OpenDB(t)
115
116 clock := &testutil.SequenceClock{
117 Times: []time.Time{
118 time.Date(2025, time.February, 2, 11, 0, 0, 0, time.FixedZone("A", 7200)),
119 time.Date(2025, time.February, 2, 12, 0, 0, 0, time.FixedZone("B", -3600)),
120 time.Date(2025, time.February, 2, 13, 0, 0, 0, time.FixedZone("C", 5400)),
121 },
122 }
123
124 store := task.NewStore(db, clock)
125 sid := "sid-update"
126
127 created, err := store.Create(ctx, sid, task.CreateParams{
128 Title: "Initial",
129 Description: "Old",
130 CreatedSeq: 1,
131 })
132 if err != nil {
133 t.Fatalf("Create: %v", err)
134 }
135
136 updated, err := store.Update(ctx, sid, created.ID, func(tk *task.Task) error {
137 tk.Title = " Updated Title "
138 tk.Description = "New"
139 return nil
140 })
141 if err != nil {
142 t.Fatalf("Update: %v", err)
143 }
144 if updated.Title != "Updated Title" {
145 t.Fatalf("Title not trimmed: %q", updated.Title)
146 }
147 if updated.Description != "New" {
148 t.Fatalf("Description not updated")
149 }
150 if !updated.UpdatedAt.Equal(clock.Times[1].UTC()) {
151 t.Fatalf("UpdatedAt mismatch: want %v got %v", clock.Times[1].UTC(), updated.UpdatedAt)
152 }
153
154 changed, err := store.UpdateStatus(ctx, sid, created.ID, task.StatusCompleted)
155 if err != nil {
156 t.Fatalf("UpdateStatus: %v", err)
157 }
158 if changed.Status != task.StatusCompleted {
159 t.Fatalf("Status not updated")
160 }
161 if !changed.UpdatedAt.Equal(clock.Times[2].UTC()) {
162 t.Fatalf("Status UpdatedAt mismatch: want %v got %v", clock.Times[2].UTC(), changed.UpdatedAt)
163 }
164
165 pending, err := store.ListByStatus(ctx, sid, task.StatusPending)
166 if err != nil {
167 t.Fatalf("ListByStatus pending: %v", err)
168 }
169 if len(pending) != 0 {
170 t.Fatalf("Pending should be empty: %+v", pending)
171 }
172
173 completed, err := store.ListByStatus(ctx, sid, task.StatusCompleted)
174 if err != nil {
175 t.Fatalf("ListByStatus completed: %v", err)
176 }
177 if len(completed) != 1 || completed[0].ID != created.ID {
178 t.Fatalf("Completed list mismatch: %+v", completed)
179 }
180}
181
182func TestStoreValidation(t *testing.T) {
183 ctx := context.Background()
184 db := testutil.OpenDB(t)
185 store := task.NewStore(db, &testutil.SequenceClock{
186 Times: []time.Time{time.Now(), time.Now()},
187 })
188
189 sid := "sid-validate"
190
191 created, err := store.Create(ctx, sid, task.CreateParams{
192 Title: "Valid",
193 Description: "desc",
194 CreatedSeq: 1,
195 })
196 if err != nil {
197 t.Fatalf("Create: %v", err)
198 }
199
200 _, err = store.Update(ctx, sid, created.ID, func(tk *task.Task) error {
201 tk.Title = " "
202 return nil
203 })
204 if !errors.Is(err, task.ErrEmptyTitle) {
205 t.Fatalf("Expected ErrEmptyTitle, got %v", err)
206 }
207
208 if _, err = store.UpdateStatus(ctx, sid, created.ID, task.Status("bogus")); !errors.Is(err, task.ErrInvalidStatus) {
209 t.Fatalf("Expected ErrInvalidStatus, got %v", err)
210 }
211}