summaryrefslogtreecommitdiff
path: root/internal/store/native_tasks_test.go
blob: 0a87bfa47201febfaf2fd9e06d48dbaf02237f74 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package store

import (
	"database/sql"
	"errors"
	"path/filepath"
	"testing"
	"time"

	"task-dashboard/internal/models"

	_ "github.com/mattn/go-sqlite3"
)

// newNativeTasksTestStore creates a Store backed by a fresh temp sqlite DB
// with just the native_tasks table -- enough to exercise
// CompleteNativeTask/UncompleteNativeTask/RescheduleNativeTask without
// running the full migration set.
func newNativeTasksTestStore(t *testing.T) *Store {
	t.Helper()
	dbPath := filepath.Join(t.TempDir(), "test.db")
	db, err := sql.Open("sqlite3", dbPath)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	if _, err := db.Exec(`
		CREATE TABLE native_tasks (
			id TEXT PRIMARY KEY,
			content TEXT NOT NULL,
			description TEXT DEFAULT '',
			project_name TEXT DEFAULT '',
			due_date DATETIME,
			priority INTEGER DEFAULT 1,
			completed BOOLEAN DEFAULT 0,
			labels TEXT DEFAULT '[]',
			created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
			updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
			recurrence_freq TEXT DEFAULT '',
			recurrence_interval INTEGER DEFAULT 1,
			recurrence_weekdays TEXT DEFAULT '',
			recurrence_series_id TEXT DEFAULT '',
			next_occurrence_override TEXT DEFAULT ''
		)
	`); err != nil {
		t.Fatal(err)
	}
	if _, err := db.Exec(`INSERT INTO native_tasks (id, content) VALUES ('real-1', 'Real task')`); err != nil {
		t.Fatal(err)
	}
	return &Store{db: db}
}

// TestCompleteNativeTask_UnknownID_ReturnsErrNotFound proves the 2026-07-12
// fix: a plain UPDATE ... WHERE id = ? silently "succeeds" with a nil error
// when 0 rows match (this is how database/sql's Exec behaves for an UPDATE
// that matches nothing -- no error, just RowsAffected() == 0). Before this
// fix, CompleteNativeTask returned that nil error straight through, so a
// stale/wrong id from a caller (the Android widget, in the real incident
// this was found from) looked identical to a real completion: HTTP 200,
// nothing changed in the database.
func TestCompleteNativeTask_UnknownID_ReturnsErrNotFound(t *testing.T) {
	s := newNativeTasksTestStore(t)

	err := s.CompleteNativeTask("does-not-exist")
	if !errors.Is(err, ErrNativeTaskNotFound) {
		t.Fatalf("expected ErrNativeTaskNotFound, got %v", err)
	}
}

func TestCompleteNativeTask_RealID_Succeeds(t *testing.T) {
	s := newNativeTasksTestStore(t)

	if err := s.CompleteNativeTask("real-1"); err != nil {
		t.Fatalf("CompleteNativeTask: %v", err)
	}

	var completed bool
	if err := s.db.QueryRow(`SELECT completed FROM native_tasks WHERE id = 'real-1'`).Scan(&completed); err != nil {
		t.Fatal(err)
	}
	if !completed {
		t.Error("expected task to be marked completed")
	}
}

func TestUncompleteNativeTask_UnknownID_ReturnsErrNotFound(t *testing.T) {
	s := newNativeTasksTestStore(t)

	err := s.UncompleteNativeTask("does-not-exist")
	if !errors.Is(err, ErrNativeTaskNotFound) {
		t.Fatalf("expected ErrNativeTaskNotFound, got %v", err)
	}
}

func TestRescheduleNativeTask_UnknownID_ReturnsErrNotFound(t *testing.T) {
	s := newNativeTasksTestStore(t)

	err := s.RescheduleNativeTask("does-not-exist", time.Now())
	if !errors.Is(err, ErrNativeTaskNotFound) {
		t.Fatalf("expected ErrNativeTaskNotFound, got %v", err)
	}
}

func TestGetNativeTasks_ParsesRecurrenceFields(t *testing.T) {
	s := newNativeTasksTestStore(t)
	if _, err := s.db.Exec(`
		INSERT INTO native_tasks (id, content, due_date, recurrence_freq, recurrence_interval, recurrence_weekdays, recurrence_series_id, next_occurrence_override)
		VALUES ('rec-1', 'Recurring task', '2026-07-13', 'weekly', 2, '1,3,5', 'series-abc', '2026-07-27')
	`); err != nil {
		t.Fatal(err)
	}

	tasks, err := s.GetNativeTasks()
	if err != nil {
		t.Fatalf("GetNativeTasks: %v", err)
	}

	var found *models.Task
	for i := range tasks {
		if tasks[i].ID == "rec-1" {
			found = &tasks[i]
		}
	}
	if found == nil {
		t.Fatal("expected to find rec-1")
	}
	if found.RecurrenceFreq != "weekly" {
		t.Errorf("RecurrenceFreq = %q, want %q", found.RecurrenceFreq, "weekly")
	}
	if found.RecurrenceInterval != 2 {
		t.Errorf("RecurrenceInterval = %d, want 2", found.RecurrenceInterval)
	}
	if len(found.RecurrenceWeekdays) != 3 || found.RecurrenceWeekdays[0] != 1 || found.RecurrenceWeekdays[1] != 3 || found.RecurrenceWeekdays[2] != 5 {
		t.Errorf("RecurrenceWeekdays = %v, want [1 3 5]", found.RecurrenceWeekdays)
	}
	if found.RecurrenceSeriesID != "series-abc" {
		t.Errorf("RecurrenceSeriesID = %q, want %q", found.RecurrenceSeriesID, "series-abc")
	}
	if found.NextOccurrenceOverride == nil || found.NextOccurrenceOverride.Format("2006-01-02") != "2026-07-27" {
		t.Errorf("NextOccurrenceOverride = %v, want 2026-07-27", found.NextOccurrenceOverride)
	}
}

func TestGetNativeTasks_NonRecurringTask_HasEmptyRecurrenceFields(t *testing.T) {
	s := newNativeTasksTestStore(t)
	// "real-1" (inserted by newNativeTasksTestStore) has no recurrence columns set.
	tasks, err := s.GetNativeTasks()
	if err != nil {
		t.Fatalf("GetNativeTasks: %v", err)
	}
	var found *models.Task
	for i := range tasks {
		if tasks[i].ID == "real-1" {
			found = &tasks[i]
		}
	}
	if found == nil {
		t.Fatal("expected to find real-1")
	}
	if found.RecurrenceSeriesID != "" {
		t.Errorf("expected empty RecurrenceSeriesID for non-recurring task, got %q", found.RecurrenceSeriesID)
	}
	if found.RecurrenceWeekdays != nil {
		t.Errorf("expected nil RecurrenceWeekdays for non-recurring task, got %v", found.RecurrenceWeekdays)
	}
	if found.NextOccurrenceOverride != nil {
		t.Errorf("expected nil NextOccurrenceOverride for non-recurring task, got %v", found.NextOccurrenceOverride)
	}
}