diff options
Diffstat (limited to 'internal/task')
| -rw-r--r-- | internal/task/operations_test.go | 133 | ||||
| -rw-r--r-- | internal/task/task.go | 71 |
2 files changed, 169 insertions, 35 deletions
diff --git a/internal/task/operations_test.go b/internal/task/operations_test.go new file mode 100644 index 0000000..7abd4bd --- /dev/null +++ b/internal/task/operations_test.go @@ -0,0 +1,133 @@ +package task + +import ( + "strings" + "testing" +) + +func TestModifyTask(t *testing.T) { + tests := []struct { + name string + id int + args []string + wantErr bool + errMsg string + }{ + { + name: "valid ID", + id: 1, + args: []string{"status:pending"}, + wantErr: false, + }, + { + name: "zero ID", + id: 0, + args: []string{"status:pending"}, + wantErr: true, + errMsg: "invalid task ID: 0", + }, + { + name: "negative ID", + id: -1, + args: []string{"status:pending"}, + wantErr: true, + errMsg: "invalid task ID: -1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := modifyTask(tt.id, tt.args...) + + // We can't test actual taskwarrior commands without it installed + // So we just test the validation + if tt.wantErr { + if err == nil { + t.Errorf("modifyTask() error = nil, wantErr %v", tt.wantErr) + } else if !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("modifyTask() error = %v, want error containing %v", err, tt.errMsg) + } + } + }) + } +} + +func TestSimpleTaskCommand(t *testing.T) { + tests := []struct { + name string + id int + command string + wantErr bool + errMsg string + }{ + { + name: "valid ID", + id: 1, + command: "done", + wantErr: false, + }, + { + name: "zero ID", + id: 0, + command: "done", + wantErr: true, + errMsg: "invalid task ID: 0", + }, + { + name: "negative ID", + id: -5, + command: "done", + wantErr: true, + errMsg: "invalid task ID: -5", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := simpleTaskCommand(tt.id, tt.command) + + // We can't test actual taskwarrior commands without it installed + // So we just test the validation + if tt.wantErr { + if err == nil { + t.Errorf("simpleTaskCommand() error = nil, wantErr %v", tt.wantErr) + } else if !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("simpleTaskCommand() error = %v, want error containing %v", err, tt.errMsg) + } + } + }) + } +} + +func TestTaskOperationsValidation(t *testing.T) { + // Test that all task operations validate IDs + invalidID := -1 + + operations := []struct { + name string + fn func() error + }{ + {"SetStatus", func() error { return SetStatus(invalidID, "pending") }}, + {"Start", func() error { return Start(invalidID) }}, + {"Stop", func() error { return Stop(invalidID) }}, + {"Done", func() error { return Done(invalidID) }}, + {"Delete", func() error { return Delete(invalidID) }}, + {"SetPriority", func() error { return SetPriority(invalidID, "H") }}, + {"SetRecurrence", func() error { return SetRecurrence(invalidID, "daily") }}, + {"SetDueDate", func() error { return SetDueDate(invalidID, "tomorrow") }}, + {"SetDescription", func() error { return SetDescription(invalidID, "test") }}, + {"Annotate", func() error { return Annotate(invalidID, "note") }}, + {"Denotate", func() error { return Denotate(invalidID, "note") }}, + } + + for _, op := range operations { + t.Run(op.name, func(t *testing.T) { + err := op.fn() + if err == nil { + t.Errorf("%s() with invalid ID = nil, want error", op.name) + } else if !strings.Contains(err.Error(), "invalid task ID") { + t.Errorf("%s() error = %v, want error containing 'invalid task ID'", op.name, err) + } + }) + } +}
\ No newline at end of file diff --git a/internal/task/task.go b/internal/task/task.go index be3b6ce..86c93f4 100644 --- a/internal/task/task.go +++ b/internal/task/task.go @@ -155,12 +155,25 @@ func run(args ...string) error { return nil } -// SetStatus changes the status of the task with the given id. -func SetStatus(id int, status string) error { +// modifyTask runs a modify command with validation +func modifyTask(id int, args ...string) error { if id <= 0 { return fmt.Errorf("invalid task ID: %d", id) } - return run(strconv.Itoa(id), "modify", "status:"+status) + return run(append([]string{strconv.Itoa(id), "modify"}, args...)...) +} + +// simpleTaskCommand runs a simple command on a task with validation +func simpleTaskCommand(id int, command string) error { + if id <= 0 { + return fmt.Errorf("invalid task ID: %d", id) + } + return run(strconv.Itoa(id), command) +} + +// SetStatus changes the status of the task with the given id. +func SetStatus(id int, status string) error { + return modifyTask(id, "status:"+status) } // SetStatusUUID changes the status of the task with the given UUID. @@ -170,42 +183,27 @@ func SetStatusUUID(uuid, status string) error { // Start begins the task with the given id. func Start(id int) error { - if id <= 0 { - return fmt.Errorf("invalid task ID: %d", id) - } - return run(strconv.Itoa(id), "start") + return simpleTaskCommand(id, "start") } // Stop stops the task with the given id. func Stop(id int) error { - if id <= 0 { - return fmt.Errorf("invalid task ID: %d", id) - } - return run(strconv.Itoa(id), "stop") + return simpleTaskCommand(id, "stop") } // Done marks the task with the given id as completed. func Done(id int) error { - if id <= 0 { - return fmt.Errorf("invalid task ID: %d", id) - } - return run(strconv.Itoa(id), "done") + return simpleTaskCommand(id, "done") } // Delete removes the task with the given id. func Delete(id int) error { - if id <= 0 { - return fmt.Errorf("invalid task ID: %d", id) - } - return run(strconv.Itoa(id), "delete") + return simpleTaskCommand(id, "delete") } // SetPriority changes the priority of the task with the given id. func SetPriority(id int, priority string) error { - if id <= 0 { - return fmt.Errorf("invalid task ID: %d", id) - } - return run(strconv.Itoa(id), "modify", "priority:"+priority) + return modifyTask(id, "priority:"+priority) } // AddTags adds tags to the task with the given id. @@ -287,26 +285,17 @@ func SetTags(id int, tags []string) error { // SetRecurrence sets the recurrence for the task with the given id. func SetRecurrence(id int, rec string) error { - if id <= 0 { - return fmt.Errorf("invalid task ID: %d", id) - } - return run(strconv.Itoa(id), "modify", "recur:"+rec) + return modifyTask(id, "recur:"+rec) } // SetDueDate sets the due date for the task with the given id. func SetDueDate(id int, due string) error { - if id <= 0 { - return fmt.Errorf("invalid task ID: %d", id) - } - return run(strconv.Itoa(id), "modify", "due:"+due) + return modifyTask(id, "due:"+due) } // SetDescription changes the description of the task with the given id. func SetDescription(id int, desc string) error { - if id <= 0 { - return fmt.Errorf("invalid task ID: %d", id) - } - return run(strconv.Itoa(id), "modify", "description:"+desc) + return modifyTask(id, "description:"+desc) } // Annotate adds an annotation to the task with the given id. @@ -388,7 +377,16 @@ func Edit(id int) error { // Started tasks are always placed before non-started ones. Tasks without a due // date are placed after tasks with a due date. Overdue tasks are placed at the // very top regardless of other properties. +// +// The sort order is: +// 1. Overdue tasks (oldest due date first) +// 2. Started tasks (not completed) +// 3. High priority tasks +// 4. Tasks with earlier due dates +// 5. Tasks sorted alphabetically by tags +// 6. Tasks sorted by ID (oldest first) func SortTasks(tasks []Task) { + // Helper to join tags in a consistent order for comparison joinTags := func(tags []string) string { if len(tags) == 0 { return "" @@ -398,6 +396,7 @@ func SortTasks(tasks []Task) { return strings.Join(cpy, " ") } + // Convert priority to numeric value for comparison (higher = more important) priVal := func(p string) int { switch p { case "H": @@ -411,6 +410,7 @@ func SortTasks(tasks []Task) { } } + // Parse due date string into time.Time parseDue := func(s string) (time.Time, bool) { if s == "" { return time.Time{}, false @@ -422,6 +422,7 @@ func SortTasks(tasks []Task) { return t, true } + // Check if a task is overdue overdue := func(t Task) bool { du, ok := parseDue(t.Due) return ok && time.Now().After(du) |
