c99a4a2d95
- Add filter.go: Parse filters (+tag, -tag, attribute:value, IDs) - Implement Filter.ToSQL() for WHERE clause generation - Add modifier.go: Parse modifiers (set/clear attributes, add/remove tags) - Implement Modifier.Apply() to update existing tasks - Add dateparse.go: Smart date parsing (ISO, today, tomorrow, weekdays) - Implement nextWeekday logic (smart Sunday interpretation) - Update GetTasks() to accept Filter parameter - Add CreateTaskWithModifier() for task creation with modifiers - Add comprehensive test suite (13 new tests, all passing) - Support filtering by status, project, priority, tags, UUIDs, display IDs - Support modifying priority, project, dates, recurrence, tags
272 lines
6.7 KiB
Go
272 lines
6.7 KiB
Go
package engine
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseModifier(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
checkFn func(*testing.T, *Modifier)
|
|
}{
|
|
{
|
|
name: "add tags",
|
|
args: []string{"+urgent", "+work"},
|
|
checkFn: func(t *testing.T, m *Modifier) {
|
|
if len(m.AddTags) != 2 {
|
|
t.Errorf("Expected 2 add tags, got %d", len(m.AddTags))
|
|
}
|
|
if m.AddTags[0] != "urgent" || m.AddTags[1] != "work" {
|
|
t.Error("Tags not parsed correctly")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "remove tags",
|
|
args: []string{"-someday", "-later"},
|
|
checkFn: func(t *testing.T, m *Modifier) {
|
|
if len(m.RemoveTags) != 2 {
|
|
t.Errorf("Expected 2 remove tags, got %d", len(m.RemoveTags))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "set attributes",
|
|
args: []string{"priority:H", "project:backend", "due:tomorrow"},
|
|
checkFn: func(t *testing.T, m *Modifier) {
|
|
if len(m.SetAttributes) != 3 {
|
|
t.Errorf("Expected 3 attributes, got %d", len(m.SetAttributes))
|
|
}
|
|
if m.SetAttributes["priority"] == nil || *m.SetAttributes["priority"] != "H" {
|
|
t.Error("Priority not set correctly")
|
|
}
|
|
if m.SetAttributes["project"] == nil || *m.SetAttributes["project"] != "backend" {
|
|
t.Error("Project not set correctly")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "clear attribute",
|
|
args: []string{"priority:"},
|
|
checkFn: func(t *testing.T, m *Modifier) {
|
|
if m.SetAttributes["priority"] != nil {
|
|
t.Error("Expected priority to be nil (cleared)")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "compound modifier",
|
|
args: []string{"+urgent", "-someday", "priority:H", "due:tomorrow"},
|
|
checkFn: func(t *testing.T, m *Modifier) {
|
|
if len(m.AddTags) != 1 || m.AddTags[0] != "urgent" {
|
|
t.Error("Add tag not parsed")
|
|
}
|
|
if len(m.RemoveTags) != 1 || m.RemoveTags[0] != "someday" {
|
|
t.Error("Remove tag not parsed")
|
|
}
|
|
if len(m.SetAttributes) != 2 {
|
|
t.Errorf("Expected 2 attributes, got %d", len(m.SetAttributes))
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := ParseModifier(tt.args)
|
|
if err != nil {
|
|
t.Fatalf("ParseModifier returned error: %v", err)
|
|
}
|
|
tt.checkFn(t, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestModifierApply(t *testing.T) {
|
|
// Create a task
|
|
task, err := CreateTask("Test task")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create task: %v", err)
|
|
}
|
|
|
|
// Create modifier
|
|
mod, _ := ParseModifier([]string{"priority:H", "project:backend", "+urgent"})
|
|
|
|
// Apply modifier
|
|
if err := mod.Apply(task); err != nil {
|
|
t.Fatalf("Failed to apply modifier: %v", err)
|
|
}
|
|
|
|
// Verify changes
|
|
if task.Priority != PriorityHigh {
|
|
t.Error("Priority not updated")
|
|
}
|
|
|
|
if task.Project == nil || *task.Project != "backend" {
|
|
t.Error("Project not updated")
|
|
}
|
|
|
|
// Reload to verify tags were saved
|
|
reloaded, _ := GetTask(task.UUID)
|
|
found := false
|
|
for _, tag := range reloaded.Tags {
|
|
if tag == "urgent" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("Tag not added")
|
|
}
|
|
}
|
|
|
|
func TestCreateTaskWithModifier(t *testing.T) {
|
|
mod, _ := ParseModifier([]string{"priority:H", "project:test", "+work", "+urgent"})
|
|
|
|
task, err := CreateTaskWithModifier("Task with modifiers", mod)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create task with modifier: %v", err)
|
|
}
|
|
|
|
if task.Priority != PriorityHigh {
|
|
t.Error("Priority not set during creation")
|
|
}
|
|
|
|
if task.Project == nil || *task.Project != "test" {
|
|
t.Error("Project not set during creation")
|
|
}
|
|
|
|
if len(task.Tags) != 2 {
|
|
t.Errorf("Expected 2 tags, got %d", len(task.Tags))
|
|
}
|
|
}
|
|
|
|
func TestParseDateISO(t *testing.T) {
|
|
date, err := ParseDate("2026-01-15")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse ISO date: %v", err)
|
|
}
|
|
|
|
if date.Year() != 2026 || date.Month() != 1 || date.Day() != 15 {
|
|
t.Errorf("Date not parsed correctly: %v", date)
|
|
}
|
|
}
|
|
|
|
func TestParseDateRelative(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
// Test today
|
|
today, err := ParseDate("today")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse 'today': %v", err)
|
|
}
|
|
if today.Day() != now.Day() {
|
|
t.Error("'today' not parsed correctly")
|
|
}
|
|
|
|
// Test tomorrow
|
|
tomorrow, err := ParseDate("tomorrow")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse 'tomorrow': %v", err)
|
|
}
|
|
expected := now.AddDate(0, 0, 1)
|
|
if tomorrow.Day() != expected.Day() {
|
|
t.Error("'tomorrow' not parsed correctly")
|
|
}
|
|
}
|
|
|
|
func TestParseDateWeekday(t *testing.T) {
|
|
// Test Sunday
|
|
sunday, err := ParseDate("sunday")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse 'sunday': %v", err)
|
|
}
|
|
if sunday.Weekday() != time.Sunday {
|
|
t.Error("'sunday' not parsed correctly")
|
|
}
|
|
|
|
// Test abbreviated form
|
|
mon, err := ParseDate("mon")
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse 'mon': %v", err)
|
|
}
|
|
if mon.Weekday() != time.Monday {
|
|
t.Error("'mon' not parsed correctly")
|
|
}
|
|
}
|
|
|
|
func TestNextWeekday(t *testing.T) {
|
|
// Test case: Thursday -> next Sunday should be this Sunday
|
|
thursday := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) // Jan 1, 2026 is a Thursday
|
|
nextSun := nextWeekday(thursday, time.Sunday)
|
|
|
|
if nextSun.Weekday() != time.Sunday {
|
|
t.Error("Should return Sunday")
|
|
}
|
|
|
|
// Should be 3 days later (this Sunday)
|
|
expectedDays := 3
|
|
actualDays := int(nextSun.Sub(thursday).Hours() / 24)
|
|
if actualDays != expectedDays {
|
|
t.Errorf("Expected %d days until Sunday, got %d", expectedDays, actualDays)
|
|
}
|
|
|
|
// Test case: Sunday -> next Sunday should be 7 days later
|
|
sunday := time.Date(2026, 1, 4, 0, 0, 0, 0, time.UTC) // Jan 4, 2026 is a Sunday
|
|
nextSun2 := nextWeekday(sunday, time.Sunday)
|
|
|
|
expectedDays = 7
|
|
actualDays = int(nextSun2.Sub(sunday).Hours() / 24)
|
|
if actualDays != expectedDays {
|
|
t.Errorf("Sunday to Sunday: expected %d days, got %d", expectedDays, actualDays)
|
|
}
|
|
}
|
|
|
|
func TestModifierWithDates(t *testing.T) {
|
|
task, _ := CreateTask("Test date modifiers")
|
|
|
|
// Apply due date
|
|
mod, _ := ParseModifier([]string{"due:tomorrow"})
|
|
if err := mod.Apply(task); err != nil {
|
|
t.Fatalf("Failed to apply date modifier: %v", err)
|
|
}
|
|
|
|
if task.Due == nil {
|
|
t.Error("Due date should be set")
|
|
}
|
|
|
|
tomorrow := time.Now().AddDate(0, 0, 1)
|
|
if task.Due.Day() != tomorrow.Day() {
|
|
t.Error("Due date not set to tomorrow")
|
|
}
|
|
|
|
// Clear due date
|
|
mod2, _ := ParseModifier([]string{"due:"})
|
|
if err := mod2.Apply(task); err != nil {
|
|
t.Fatalf("Failed to clear due date: %v", err)
|
|
}
|
|
|
|
if task.Due != nil {
|
|
t.Error("Due date should be cleared")
|
|
}
|
|
}
|
|
|
|
func TestModifierWithRecurrence(t *testing.T) {
|
|
task, _ := CreateTask("Test recurrence modifier")
|
|
|
|
mod, _ := ParseModifier([]string{"recur:1w"})
|
|
if err := mod.Apply(task); err != nil {
|
|
t.Fatalf("Failed to apply recurrence: %v", err)
|
|
}
|
|
|
|
if task.RecurrenceDuration == nil {
|
|
t.Error("Recurrence should be set")
|
|
}
|
|
|
|
expected := 7 * 24 * time.Hour
|
|
if *task.RecurrenceDuration != expected {
|
|
t.Errorf("Expected recurrence %v, got %v", expected, *task.RecurrenceDuration)
|
|
}
|
|
}
|