integration_test.go

  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}