1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5//go:build integration
6
7package lunatask_test
8
9import (
10 "os"
11 "testing"
12 "time"
13
14 lunatask "git.secluded.site/go-lunatask"
15)
16
17var (
18 integrationClient *lunatask.Client
19 testAreaID string
20)
21
22func TestMain(m *testing.M) {
23 apiKey := os.Getenv("LUNATASK_API_KEY")
24 if apiKey == "" {
25 panic("LUNATASK_API_KEY environment variable required for integration tests")
26 }
27
28 testAreaID = os.Getenv("LUNATASK_TEST_AREA")
29 if testAreaID == "" {
30 panic("LUNATASK_TEST_AREA environment variable required for integration tests")
31 }
32
33 integrationClient = lunatask.NewClient(apiKey)
34
35 os.Exit(m.Run())
36}
37
38// testName generates a unique name for test entities.
39func testName(prefix string) string {
40 return prefix + "-" + time.Now().Format("20060102-150405.000")
41}
42
43func TestIntegration_Ping(t *testing.T) {
44 resp, err := integrationClient.Ping(ctx())
45 if err != nil {
46 t.Fatalf("Ping() error = %v", err)
47 }
48
49 t.Logf("Ping response: %+v", resp)
50}
51
52func TestIntegration_ListTasks(t *testing.T) {
53 tasks, err := integrationClient.ListTasks(ctx(), nil)
54 if err != nil {
55 t.Fatalf("ListTasks() error = %v", err)
56 }
57
58 t.Logf("Found %d tasks", len(tasks))
59}
60
61func TestIntegration_ListNotes(t *testing.T) {
62 notes, err := integrationClient.ListNotes(ctx(), nil)
63 if err != nil {
64 t.Fatalf("ListNotes() error = %v", err)
65 }
66
67 t.Logf("Found %d notes", len(notes))
68}
69
70func TestIntegration_ListPeople(t *testing.T) {
71 people, err := integrationClient.ListPeople(ctx(), nil)
72 if err != nil {
73 t.Fatalf("ListPeople() error = %v", err)
74 }
75
76 t.Logf("Found %d people", len(people))
77}
78
79func TestIntegration_TaskRoundTrip(t *testing.T) {
80 name := testName("integration-task")
81
82 task, err := integrationClient.NewTask(name).
83 InArea(testAreaID).
84 WithStatus(lunatask.StatusNext).
85 WithEstimate(15).
86 Create(ctx())
87 if err != nil {
88 t.Fatalf("NewTask().Create() error = %v", err)
89 }
90
91 if task == nil {
92 t.Fatal("NewTask().Create() returned nil (duplicate?)")
93 }
94
95 t.Cleanup(func() {
96 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
97 t.Errorf("DeleteTask() cleanup error = %v", err)
98 }
99 })
100
101 t.Logf("Created task: %s", task.ID)
102
103 // Verify we can fetch it
104 fetched, err := integrationClient.GetTask(ctx(), task.ID)
105 if err != nil {
106 t.Fatalf("GetTask() error = %v", err)
107 }
108
109 if fetched.ID != task.ID {
110 t.Errorf("GetTask().ID = %s, want %s", fetched.ID, task.ID)
111 }
112
113 // Update it
114 updated, err := integrationClient.NewTaskUpdate(task.ID).
115 WithEstimate(30).
116 Update(ctx())
117 if err != nil {
118 t.Fatalf("NewTaskUpdate().Update() error = %v", err)
119 }
120
121 if updated.Estimate == nil || *updated.Estimate != 30 {
122 t.Errorf("updated estimate = %v, want 30", updated.Estimate)
123 }
124
125 t.Logf("Updated task estimate to 30 minutes")
126}
127
128func TestIntegration_NoteRoundTrip(t *testing.T) {
129 name := testName("integration-note")
130
131 note, err := integrationClient.NewNote().
132 WithName(name).
133 WithContent("# Test Note\n\nThis is a test.").
134 Create(ctx())
135 if err != nil {
136 t.Fatalf("NewNote().Create() error = %v", err)
137 }
138
139 if note == nil {
140 t.Fatal("NewNote().Create() returned nil (duplicate?)")
141 }
142
143 t.Cleanup(func() {
144 if _, err := integrationClient.DeleteNote(ctx(), note.ID); err != nil {
145 t.Errorf("DeleteNote() cleanup error = %v", err)
146 }
147 })
148
149 t.Logf("Created note: %s", note.ID)
150
151 // Verify we can fetch it
152 fetched, err := integrationClient.GetNote(ctx(), note.ID)
153 if err != nil {
154 t.Fatalf("GetNote() error = %v", err)
155 }
156
157 if fetched.ID != note.ID {
158 t.Errorf("GetNote().ID = %s, want %s", fetched.ID, note.ID)
159 }
160
161 // Update it
162 updated, err := integrationClient.NewNoteUpdate(note.ID).
163 WithContent("# Updated\n\nNew content.").
164 Update(ctx())
165 if err != nil {
166 t.Fatalf("NewNoteUpdate().Update() error = %v", err)
167 }
168
169 t.Logf("Updated note: %s", updated.ID)
170}
171
172// TestIntegration_TaskWorkflowMetadata tests how the API handles various combinations
173// of workflow metadata fields. This helps determine whether lune needs to warn users
174// about workflow restrictions.
175func TestIntegration_TaskWorkflowMetadata(t *testing.T) {
176 t.Run("all_metadata_fields", func(t *testing.T) {
177 name := testName("metadata-all")
178 task, err := integrationClient.NewTask(name).
179 InArea(testAreaID).
180 WithStatus(lunatask.StatusNext).
181 WithEisenhower(lunatask.EisenhowerDoNow).
182 Priority(lunatask.PriorityHigh).
183 WithMotivation(lunatask.MotivationMust).
184 WithEstimate(60).
185 Create(ctx())
186 if err != nil {
187 t.Fatalf("Create() error = %v", err)
188 }
189 if task == nil {
190 t.Fatal("Create() returned nil (duplicate?)")
191 }
192
193 t.Cleanup(func() {
194 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
195 t.Errorf("DeleteTask() cleanup error = %v", err)
196 }
197 })
198
199 fetched, err := integrationClient.GetTask(ctx(), task.ID)
200 if err != nil {
201 t.Fatalf("GetTask() error = %v", err)
202 }
203
204 logMetadata(t, "all_fields", fetched)
205 if fetched.Status == nil || *fetched.Status != lunatask.StatusNext {
206 t.Errorf("Status = %v, want %v", fetched.Status, lunatask.StatusNext)
207 }
208 if fetched.Eisenhower == nil || *fetched.Eisenhower != lunatask.EisenhowerDoNow {
209 t.Errorf("Eisenhower = %v, want %v", fetched.Eisenhower, lunatask.EisenhowerDoNow)
210 }
211 if fetched.Priority == nil || *fetched.Priority != lunatask.PriorityHigh {
212 t.Errorf("Priority = %v, want %v", fetched.Priority, lunatask.PriorityHigh)
213 }
214 if fetched.Motivation == nil || *fetched.Motivation != lunatask.MotivationMust {
215 t.Errorf("Motivation = %v, want %v", fetched.Motivation, lunatask.MotivationMust)
216 }
217 })
218
219 t.Run("eisenhower_only", func(t *testing.T) {
220 name := testName("metadata-eisenhower")
221 task, err := integrationClient.NewTask(name).
222 InArea(testAreaID).
223 WithEisenhower(lunatask.EisenhowerDoLater).
224 Create(ctx())
225 if err != nil {
226 t.Fatalf("Create() error = %v", err)
227 }
228 if task == nil {
229 t.Fatal("Create() returned nil (duplicate?)")
230 }
231
232 t.Cleanup(func() {
233 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
234 t.Errorf("DeleteTask() cleanup error = %v", err)
235 }
236 })
237
238 fetched, err := integrationClient.GetTask(ctx(), task.ID)
239 if err != nil {
240 t.Fatalf("GetTask() error = %v", err)
241 }
242
243 logMetadata(t, "eisenhower_only", fetched)
244 if fetched.Eisenhower == nil || *fetched.Eisenhower != lunatask.EisenhowerDoLater {
245 t.Errorf("Eisenhower = %v, want %v", fetched.Eisenhower, lunatask.EisenhowerDoLater)
246 }
247 })
248
249 t.Run("kanban_status_only", func(t *testing.T) {
250 name := testName("metadata-kanban")
251 task, err := integrationClient.NewTask(name).
252 InArea(testAreaID).
253 WithStatus(lunatask.StatusWaiting).
254 Create(ctx())
255 if err != nil {
256 t.Fatalf("Create() error = %v", err)
257 }
258 if task == nil {
259 t.Fatal("Create() returned nil (duplicate?)")
260 }
261
262 t.Cleanup(func() {
263 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
264 t.Errorf("DeleteTask() cleanup error = %v", err)
265 }
266 })
267
268 fetched, err := integrationClient.GetTask(ctx(), task.ID)
269 if err != nil {
270 t.Fatalf("GetTask() error = %v", err)
271 }
272
273 logMetadata(t, "kanban_only", fetched)
274 if fetched.Status == nil || *fetched.Status != lunatask.StatusWaiting {
275 t.Errorf("Status = %v, want %v", fetched.Status, lunatask.StatusWaiting)
276 }
277 })
278
279 t.Run("priority_and_motivation", func(t *testing.T) {
280 name := testName("metadata-pri-mot")
281 task, err := integrationClient.NewTask(name).
282 InArea(testAreaID).
283 Priority(lunatask.PriorityHighest).
284 WithMotivation(lunatask.MotivationWant).
285 Create(ctx())
286 if err != nil {
287 t.Fatalf("Create() error = %v", err)
288 }
289 if task == nil {
290 t.Fatal("Create() returned nil (duplicate?)")
291 }
292
293 t.Cleanup(func() {
294 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
295 t.Errorf("DeleteTask() cleanup error = %v", err)
296 }
297 })
298
299 fetched, err := integrationClient.GetTask(ctx(), task.ID)
300 if err != nil {
301 t.Fatalf("GetTask() error = %v", err)
302 }
303
304 logMetadata(t, "priority_motivation", fetched)
305 if fetched.Priority == nil || *fetched.Priority != lunatask.PriorityHighest {
306 t.Errorf("Priority = %v, want %v", fetched.Priority, lunatask.PriorityHighest)
307 }
308 if fetched.Motivation == nil || *fetched.Motivation != lunatask.MotivationWant {
309 t.Errorf("Motivation = %v, want %v", fetched.Motivation, lunatask.MotivationWant)
310 }
311 })
312
313 t.Run("all_eisenhower_quadrants", func(t *testing.T) {
314 quadrants := []struct {
315 name string
316 quadrant lunatask.Eisenhower
317 important bool
318 urgent bool
319 }{
320 {"do_now", lunatask.EisenhowerDoNow, true, true},
321 {"delegate", lunatask.EisenhowerDelegate, false, true},
322 {"do_later", lunatask.EisenhowerDoLater, true, false},
323 {"eliminate", lunatask.EisenhowerEliminate, false, false},
324 }
325
326 for _, q := range quadrants {
327 t.Run(q.name, func(t *testing.T) {
328 name := testName("eisenhower-" + q.name)
329 task, err := integrationClient.NewTask(name).
330 InArea(testAreaID).
331 WithEisenhower(q.quadrant).
332 Create(ctx())
333 if err != nil {
334 t.Fatalf("Create() error = %v", err)
335 }
336 if task == nil {
337 t.Fatal("Create() returned nil (duplicate?)")
338 }
339
340 t.Cleanup(func() {
341 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
342 t.Errorf("DeleteTask() cleanup error = %v", err)
343 }
344 })
345
346 fetched, err := integrationClient.GetTask(ctx(), task.ID)
347 if err != nil {
348 t.Fatalf("GetTask() error = %v", err)
349 }
350
351 t.Logf("Quadrant %s: eisenhower=%v", q.name, fetched.Eisenhower)
352 if fetched.Eisenhower == nil || *fetched.Eisenhower != q.quadrant {
353 t.Errorf("Eisenhower = %v, want %v", fetched.Eisenhower, q.quadrant)
354 }
355 })
356 }
357 })
358
359 t.Run("all_status_values", func(t *testing.T) {
360 statuses := []lunatask.TaskStatus{
361 lunatask.StatusLater,
362 lunatask.StatusNext,
363 lunatask.StatusInProgress,
364 lunatask.StatusWaiting,
365 }
366
367 for _, status := range statuses {
368 t.Run(string(status), func(t *testing.T) {
369 name := testName("status-" + string(status))
370 task, err := integrationClient.NewTask(name).
371 InArea(testAreaID).
372 WithStatus(status).
373 Create(ctx())
374 if err != nil {
375 t.Fatalf("Create() error = %v", err)
376 }
377 if task == nil {
378 t.Fatal("Create() returned nil (duplicate?)")
379 }
380
381 t.Cleanup(func() {
382 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
383 t.Errorf("DeleteTask() cleanup error = %v", err)
384 }
385 })
386
387 fetched, err := integrationClient.GetTask(ctx(), task.ID)
388 if err != nil {
389 t.Fatalf("GetTask() error = %v", err)
390 }
391
392 t.Logf("Status %s: status=%v", status, fetched.Status)
393 if fetched.Status == nil || *fetched.Status != status {
394 t.Errorf("Status = %v, want %v", fetched.Status, status)
395 }
396 })
397 }
398 })
399
400 t.Run("update_clears_eisenhower", func(t *testing.T) {
401 name := testName("clear-eisenhower")
402 task, err := integrationClient.NewTask(name).
403 InArea(testAreaID).
404 WithEisenhower(lunatask.EisenhowerDoNow).
405 Create(ctx())
406 if err != nil {
407 t.Fatalf("Create() error = %v", err)
408 }
409 if task == nil {
410 t.Fatal("Create() returned nil (duplicate?)")
411 }
412
413 t.Cleanup(func() {
414 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
415 t.Errorf("DeleteTask() cleanup error = %v", err)
416 }
417 })
418
419 updated, err := integrationClient.NewTaskUpdate(task.ID).
420 WithEisenhower(lunatask.EisenhowerUncategorized).
421 Update(ctx())
422 if err != nil {
423 t.Fatalf("Update() error = %v", err)
424 }
425
426 t.Logf("After clear: eisenhower=%v", updated.Eisenhower)
427 if updated.Eisenhower == nil || *updated.Eisenhower != lunatask.EisenhowerUncategorized {
428 t.Errorf("Eisenhower = %v, want %v (uncategorized)", updated.Eisenhower, lunatask.EisenhowerUncategorized)
429 }
430 })
431
432 t.Run("update_preserves_unmodified_fields", func(t *testing.T) {
433 name := testName("preserve-fields")
434 task, err := integrationClient.NewTask(name).
435 InArea(testAreaID).
436 WithStatus(lunatask.StatusNext).
437 WithEisenhower(lunatask.EisenhowerDoLater).
438 Priority(lunatask.PriorityHigh).
439 Create(ctx())
440 if err != nil {
441 t.Fatalf("Create() error = %v", err)
442 }
443 if task == nil {
444 t.Fatal("Create() returned nil (duplicate?)")
445 }
446
447 t.Cleanup(func() {
448 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
449 t.Errorf("DeleteTask() cleanup error = %v", err)
450 }
451 })
452
453 updated, err := integrationClient.NewTaskUpdate(task.ID).
454 WithMotivation(lunatask.MotivationShould).
455 Update(ctx())
456 if err != nil {
457 t.Fatalf("Update() error = %v", err)
458 }
459
460 logMetadata(t, "after_update", updated)
461
462 if updated.Status == nil || *updated.Status != lunatask.StatusNext {
463 t.Errorf("Status changed: %v, want %v", updated.Status, lunatask.StatusNext)
464 }
465 if updated.Eisenhower == nil || *updated.Eisenhower != lunatask.EisenhowerDoLater {
466 t.Errorf("Eisenhower changed: %v, want %v", updated.Eisenhower, lunatask.EisenhowerDoLater)
467 }
468 if updated.Priority == nil || *updated.Priority != lunatask.PriorityHigh {
469 t.Errorf("Priority changed: %v, want %v", updated.Priority, lunatask.PriorityHigh)
470 }
471 if updated.Motivation == nil || *updated.Motivation != lunatask.MotivationShould {
472 t.Errorf("Motivation = %v, want %v", updated.Motivation, lunatask.MotivationShould)
473 }
474 })
475
476 t.Run("cross_workflow_update", func(t *testing.T) {
477 name := testName("cross-workflow")
478 task, err := integrationClient.NewTask(name).
479 InArea(testAreaID).
480 WithEisenhower(lunatask.EisenhowerDoNow).
481 Create(ctx())
482 if err != nil {
483 t.Fatalf("Create() error = %v", err)
484 }
485 if task == nil {
486 t.Fatal("Create() returned nil (duplicate?)")
487 }
488
489 t.Cleanup(func() {
490 if _, err := integrationClient.DeleteTask(ctx(), task.ID); err != nil {
491 t.Errorf("DeleteTask() cleanup error = %v", err)
492 }
493 })
494
495 logMetadata(t, "after_create", task)
496
497 updated, err := integrationClient.NewTaskUpdate(task.ID).
498 WithStatus(lunatask.StatusWaiting).
499 WithMotivation(lunatask.MotivationMust).
500 Update(ctx())
501 if err != nil {
502 t.Fatalf("Update() error = %v", err)
503 }
504
505 logMetadata(t, "after_update", updated)
506
507 if updated.Eisenhower == nil || *updated.Eisenhower != lunatask.EisenhowerDoNow {
508 t.Errorf("Eisenhower lost after update: %v, want %v", updated.Eisenhower, lunatask.EisenhowerDoNow)
509 }
510 if updated.Status == nil || *updated.Status != lunatask.StatusWaiting {
511 t.Errorf("Status = %v, want %v", updated.Status, lunatask.StatusWaiting)
512 }
513 if updated.Motivation == nil || *updated.Motivation != lunatask.MotivationMust {
514 t.Errorf("Motivation = %v, want %v", updated.Motivation, lunatask.MotivationMust)
515 }
516
517 fetched, err := integrationClient.GetTask(ctx(), task.ID)
518 if err != nil {
519 t.Fatalf("GetTask() error = %v", err)
520 }
521
522 logMetadata(t, "after_fetch", fetched)
523
524 if fetched.Eisenhower == nil || *fetched.Eisenhower != lunatask.EisenhowerDoNow {
525 t.Errorf("Eisenhower lost after fetch: %v, want %v", fetched.Eisenhower, lunatask.EisenhowerDoNow)
526 }
527 if fetched.Status == nil || *fetched.Status != lunatask.StatusWaiting {
528 t.Errorf("Status after fetch = %v, want %v", fetched.Status, lunatask.StatusWaiting)
529 }
530 if fetched.Motivation == nil || *fetched.Motivation != lunatask.MotivationMust {
531 t.Errorf("Motivation after fetch = %v, want %v", fetched.Motivation, lunatask.MotivationMust)
532 }
533 })
534}
535
536func logMetadata(t *testing.T, label string, task *lunatask.Task) {
537 t.Helper()
538 t.Logf("%s: status=%v eisenhower=%v priority=%v motivation=%v",
539 label, task.Status, task.Eisenhower, task.Priority, task.Motivation)
540}