Implement opal-task Phase 2: Core Task Model CRUD
- Add complete CRUD operations: CreateTask, GetTask, GetTasks, Save - Implement tag operations: AddTag, RemoveTag, GetTags - Add task lifecycle methods: Complete, Delete, StartTask, StopTask - Implement SQL type conversion helpers for nullable fields - Add comprehensive test suite with 9 passing tests - Fix timestamp handling (Created/Modified as Unix timestamps) - Implement soft delete (status change) and hard delete options
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -55,3 +56,529 @@ type Task struct {
|
||||
|
||||
// timeNow returns current time (allows mocking in tests)
|
||||
var timeNow = time.Now
|
||||
|
||||
// Helper functions for SQL time conversion
|
||||
|
||||
func timeToSQL(t *time.Time) interface{} {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return t.Unix()
|
||||
}
|
||||
|
||||
func sqlToTime(v interface{}) *time.Time {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
unix, ok := v.(int64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
t := time.Unix(unix, 0)
|
||||
return &t
|
||||
}
|
||||
|
||||
func durationToSQL(d *time.Duration) interface{} {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
return int64(*d)
|
||||
}
|
||||
|
||||
func sqlToDuration(v interface{}) *time.Duration {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
nanos, ok := v.(int64)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
d := time.Duration(nanos)
|
||||
return &d
|
||||
}
|
||||
|
||||
func stringPtrToSQL(s *string) interface{} {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
func sqlToStringPtr(v interface{}) *string {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &str
|
||||
}
|
||||
|
||||
func uuidPtrToSQL(u *uuid.UUID) interface{} {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func sqlToUUIDPtr(v interface{}) *uuid.UUID {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
u, err := uuid.Parse(str)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
// CreateTask creates a new task with the given description
|
||||
func CreateTask(description string) (*Task, error) {
|
||||
now := timeNow()
|
||||
task := &Task{
|
||||
UUID: uuid.New(),
|
||||
Status: StatusPending,
|
||||
Description: description,
|
||||
Priority: PriorityDefault,
|
||||
Created: now,
|
||||
Modified: now,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
if err := task.Save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// GetTask retrieves a task by UUID
|
||||
func GetTask(taskUUID uuid.UUID) (*Task, error) {
|
||||
db := GetDB()
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, uuid, status, description, project, priority,
|
||||
created, modified, start, end, due, scheduled, wait, until_date,
|
||||
recurrence_duration, parent_uuid
|
||||
FROM tasks
|
||||
WHERE uuid = ?
|
||||
`
|
||||
|
||||
task := &Task{}
|
||||
var (
|
||||
uuidStr string
|
||||
project interface{}
|
||||
created int64
|
||||
modified int64
|
||||
start interface{}
|
||||
end interface{}
|
||||
due interface{}
|
||||
scheduled interface{}
|
||||
wait interface{}
|
||||
until interface{}
|
||||
recurDuration interface{}
|
||||
parentUUIDStr interface{}
|
||||
)
|
||||
|
||||
err := db.QueryRow(query, taskUUID.String()).Scan(
|
||||
&task.ID,
|
||||
&uuidStr,
|
||||
&task.Status,
|
||||
&task.Description,
|
||||
&project,
|
||||
&task.Priority,
|
||||
&created,
|
||||
&modified,
|
||||
&start,
|
||||
&end,
|
||||
&due,
|
||||
&scheduled,
|
||||
&wait,
|
||||
&until,
|
||||
&recurDuration,
|
||||
&parentUUIDStr,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get task: %w", err)
|
||||
}
|
||||
|
||||
// Parse UUID
|
||||
task.UUID, err = uuid.Parse(uuidStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse UUID: %w", err)
|
||||
}
|
||||
|
||||
// Convert timestamps
|
||||
task.Created = time.Unix(created, 0)
|
||||
task.Modified = time.Unix(modified, 0)
|
||||
|
||||
// Convert nullable fields
|
||||
task.Project = sqlToStringPtr(project)
|
||||
task.Start = sqlToTime(start)
|
||||
task.End = sqlToTime(end)
|
||||
task.Due = sqlToTime(due)
|
||||
task.Scheduled = sqlToTime(scheduled)
|
||||
task.Wait = sqlToTime(wait)
|
||||
task.Until = sqlToTime(until)
|
||||
task.RecurrenceDuration = sqlToDuration(recurDuration)
|
||||
task.ParentUUID = sqlToUUIDPtr(parentUUIDStr)
|
||||
|
||||
// Load tags
|
||||
tags, err := task.GetTags()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load tags: %w", err)
|
||||
}
|
||||
task.Tags = tags
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
// GetTasks retrieves all tasks (filtering will be added later)
|
||||
func GetTasks() ([]*Task, error) {
|
||||
db := GetDB()
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, uuid, status, description, project, priority,
|
||||
created, modified, start, end, due, scheduled, wait, until_date,
|
||||
recurrence_duration, parent_uuid
|
||||
FROM tasks
|
||||
ORDER BY due ASC, priority DESC
|
||||
`
|
||||
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query tasks: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tasks := []*Task{}
|
||||
|
||||
for rows.Next() {
|
||||
task := &Task{}
|
||||
var (
|
||||
uuidStr string
|
||||
project interface{}
|
||||
created int64
|
||||
modified int64
|
||||
start interface{}
|
||||
end interface{}
|
||||
due interface{}
|
||||
scheduled interface{}
|
||||
wait interface{}
|
||||
until interface{}
|
||||
recurDuration interface{}
|
||||
parentUUIDStr interface{}
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
&task.ID,
|
||||
&uuidStr,
|
||||
&task.Status,
|
||||
&task.Description,
|
||||
&project,
|
||||
&task.Priority,
|
||||
&created,
|
||||
&modified,
|
||||
&start,
|
||||
&end,
|
||||
&due,
|
||||
&scheduled,
|
||||
&wait,
|
||||
&until,
|
||||
&recurDuration,
|
||||
&parentUUIDStr,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan task: %w", err)
|
||||
}
|
||||
|
||||
// Parse UUID
|
||||
task.UUID, err = uuid.Parse(uuidStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse UUID: %w", err)
|
||||
}
|
||||
|
||||
// Convert timestamps
|
||||
task.Created = time.Unix(created, 0)
|
||||
task.Modified = time.Unix(modified, 0)
|
||||
|
||||
// Convert nullable fields
|
||||
task.Project = sqlToStringPtr(project)
|
||||
task.Start = sqlToTime(start)
|
||||
task.End = sqlToTime(end)
|
||||
task.Due = sqlToTime(due)
|
||||
task.Scheduled = sqlToTime(scheduled)
|
||||
task.Wait = sqlToTime(wait)
|
||||
task.Until = sqlToTime(until)
|
||||
task.RecurrenceDuration = sqlToDuration(recurDuration)
|
||||
task.ParentUUID = sqlToUUIDPtr(parentUUIDStr)
|
||||
|
||||
// Load tags
|
||||
tags, err := task.GetTags()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load tags: %w", err)
|
||||
}
|
||||
task.Tags = tags
|
||||
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// Save inserts or updates the task in the database
|
||||
func (t *Task) Save() error {
|
||||
db := GetDB()
|
||||
if db == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
t.Modified = timeNow()
|
||||
|
||||
// Check if task exists
|
||||
var exists bool
|
||||
err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM tasks WHERE uuid = ?)", t.UUID.String()).Scan(&exists)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if task exists: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
// Update existing task
|
||||
query := `
|
||||
UPDATE tasks SET
|
||||
status = ?, description = ?, project = ?, priority = ?,
|
||||
modified = ?, start = ?, end = ?, due = ?,
|
||||
scheduled = ?, wait = ?, until_date = ?,
|
||||
recurrence_duration = ?, parent_uuid = ?
|
||||
WHERE uuid = ?
|
||||
`
|
||||
|
||||
_, err = db.Exec(query,
|
||||
t.Status,
|
||||
t.Description,
|
||||
stringPtrToSQL(t.Project),
|
||||
t.Priority,
|
||||
t.Modified.Unix(),
|
||||
timeToSQL(t.Start),
|
||||
timeToSQL(t.End),
|
||||
timeToSQL(t.Due),
|
||||
timeToSQL(t.Scheduled),
|
||||
timeToSQL(t.Wait),
|
||||
timeToSQL(t.Until),
|
||||
durationToSQL(t.RecurrenceDuration),
|
||||
uuidPtrToSQL(t.ParentUUID),
|
||||
t.UUID.String(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update task: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Insert new task
|
||||
query := `
|
||||
INSERT INTO tasks (
|
||||
uuid, status, description, project, priority,
|
||||
created, modified, start, end, due, scheduled, wait, until_date,
|
||||
recurrence_duration, parent_uuid
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
result, err := db.Exec(query,
|
||||
t.UUID.String(),
|
||||
t.Status,
|
||||
t.Description,
|
||||
stringPtrToSQL(t.Project),
|
||||
t.Priority,
|
||||
t.Created.Unix(),
|
||||
t.Modified.Unix(),
|
||||
timeToSQL(t.Start),
|
||||
timeToSQL(t.End),
|
||||
timeToSQL(t.Due),
|
||||
timeToSQL(t.Scheduled),
|
||||
timeToSQL(t.Wait),
|
||||
timeToSQL(t.Until),
|
||||
durationToSQL(t.RecurrenceDuration),
|
||||
uuidPtrToSQL(t.ParentUUID),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert task: %w", err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get last insert id: %w", err)
|
||||
}
|
||||
t.ID = int(id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddTag adds a tag to the task
|
||||
func (t *Task) AddTag(tag string) error {
|
||||
db := GetDB()
|
||||
if db == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
// Ensure task has been saved and has an ID
|
||||
if t.ID == 0 {
|
||||
return fmt.Errorf("task must be saved before adding tags")
|
||||
}
|
||||
|
||||
_, err := db.Exec("INSERT OR IGNORE INTO tags (task_id, tag) VALUES (?, ?)", t.ID, tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add tag: %w", err)
|
||||
}
|
||||
|
||||
// Update in-memory tags
|
||||
for _, existingTag := range t.Tags {
|
||||
if existingTag == tag {
|
||||
return nil // Already exists
|
||||
}
|
||||
}
|
||||
t.Tags = append(t.Tags, tag)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTag removes a tag from the task
|
||||
func (t *Task) RemoveTag(tag string) error {
|
||||
db := GetDB()
|
||||
if db == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
if t.ID == 0 {
|
||||
return fmt.Errorf("task must be saved before removing tags")
|
||||
}
|
||||
|
||||
_, err := db.Exec("DELETE FROM tags WHERE task_id = ? AND tag = ?", t.ID, tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove tag: %w", err)
|
||||
}
|
||||
|
||||
// Update in-memory tags
|
||||
newTags := []string{}
|
||||
for _, existingTag := range t.Tags {
|
||||
if existingTag != tag {
|
||||
newTags = append(newTags, existingTag)
|
||||
}
|
||||
}
|
||||
t.Tags = newTags
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTags retrieves all tags for the task
|
||||
func (t *Task) GetTags() ([]string, error) {
|
||||
db := GetDB()
|
||||
if db == nil {
|
||||
return nil, fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
if t.ID == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
rows, err := db.Query("SELECT tag FROM tags WHERE task_id = ? ORDER BY tag", t.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get tags: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tags := []string{}
|
||||
for rows.Next() {
|
||||
var tag string
|
||||
if err := rows.Scan(&tag); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan tag: %w", err)
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// Complete marks a task as completed
|
||||
func (t *Task) Complete() error {
|
||||
t.Status = StatusCompleted
|
||||
now := timeNow()
|
||||
t.End = &now
|
||||
|
||||
if err := t.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this is a recurring instance, spawn next instance
|
||||
if t.ParentUUID != nil {
|
||||
if err := SpawnNextInstance(t); err != nil {
|
||||
// Log error but don't fail the completion
|
||||
return fmt.Errorf("completed task but failed to spawn next instance: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete marks a task as deleted (soft delete)
|
||||
func (t *Task) Delete(permanent bool) error {
|
||||
db := GetDB()
|
||||
if db == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
if permanent {
|
||||
// Hard delete - remove from database
|
||||
_, err := db.Exec("DELETE FROM tasks WHERE uuid = ?", t.UUID.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete task: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Soft delete - mark as deleted
|
||||
t.Status = StatusDeleted
|
||||
now := timeNow()
|
||||
t.End = &now
|
||||
if err := t.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartTask sets the start time for a task
|
||||
func (t *Task) StartTask() error {
|
||||
now := timeNow()
|
||||
t.Start = &now
|
||||
return t.Save()
|
||||
}
|
||||
|
||||
// StopTask clears the start time for a task
|
||||
func (t *Task) StopTask() error {
|
||||
t.Start = nil
|
||||
return t.Save()
|
||||
}
|
||||
|
||||
// IsRecurringTemplate returns true if task is a recurring template
|
||||
func (t *Task) IsRecurringTemplate() bool {
|
||||
return t.Status == StatusRecurring && t.RecurrenceDuration != nil && t.ParentUUID == nil
|
||||
}
|
||||
|
||||
// IsRecurringInstance returns true if task is a recurring instance
|
||||
func (t *Task) IsRecurringInstance() bool {
|
||||
return t.ParentUUID != nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Setup test database
|
||||
os.Setenv("HOME", "/tmp/opal-test")
|
||||
|
||||
// Ensure config directory exists
|
||||
configDir := "/tmp/opal-test/.config/jade"
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := InitDB(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer CloseDB()
|
||||
|
||||
// Run tests
|
||||
code := m.Run()
|
||||
|
||||
// Cleanup
|
||||
os.RemoveAll("/tmp/opal-test/.config")
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestCreateTask(t *testing.T) {
|
||||
task, err := CreateTask("Test task")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
if task.UUID == uuid.Nil {
|
||||
t.Error("Task UUID should not be nil")
|
||||
}
|
||||
|
||||
if task.Description != "Test task" {
|
||||
t.Errorf("Expected description 'Test task', got '%s'", task.Description)
|
||||
}
|
||||
|
||||
if task.Status != StatusPending {
|
||||
t.Errorf("Expected status Pending, got %v", task.Status)
|
||||
}
|
||||
|
||||
if task.Priority != PriorityDefault {
|
||||
t.Errorf("Expected priority Default, got %v", task.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTask(t *testing.T) {
|
||||
// Create a task
|
||||
created, err := CreateTask("Test get task")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve it
|
||||
retrieved, err := GetTask(created.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get task: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.UUID != created.UUID {
|
||||
t.Error("UUIDs don't match")
|
||||
}
|
||||
|
||||
if retrieved.Description != created.Description {
|
||||
t.Error("Descriptions don't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSave(t *testing.T) {
|
||||
task, err := CreateTask("Test save")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
// Modify task
|
||||
task.Description = "Modified description"
|
||||
task.Priority = PriorityHigh
|
||||
project := "test-project"
|
||||
task.Project = &project
|
||||
|
||||
if err := task.Save(); err != nil {
|
||||
t.Fatalf("Failed to save task: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve and verify
|
||||
retrieved, err := GetTask(task.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get task: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Description != "Modified description" {
|
||||
t.Error("Description not updated")
|
||||
}
|
||||
|
||||
if retrieved.Priority != PriorityHigh {
|
||||
t.Error("Priority not updated")
|
||||
}
|
||||
|
||||
if retrieved.Project == nil || *retrieved.Project != "test-project" {
|
||||
t.Error("Project not updated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskTags(t *testing.T) {
|
||||
task, err := CreateTask("Test tags")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
// Add tags
|
||||
if err := task.AddTag("urgent"); err != nil {
|
||||
t.Fatalf("Failed to add tag: %v", err)
|
||||
}
|
||||
|
||||
if err := task.AddTag("work"); err != nil {
|
||||
t.Fatalf("Failed to add tag: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve and verify tags
|
||||
retrieved, err := GetTask(task.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get task: %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved.Tags) != 2 {
|
||||
t.Errorf("Expected 2 tags, got %d", len(retrieved.Tags))
|
||||
}
|
||||
|
||||
// Remove a tag
|
||||
if err := retrieved.RemoveTag("urgent"); err != nil {
|
||||
t.Fatalf("Failed to remove tag: %v", err)
|
||||
}
|
||||
|
||||
// Verify removal
|
||||
retrieved2, err := GetTask(task.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get task: %v", err)
|
||||
}
|
||||
|
||||
if len(retrieved2.Tags) != 1 {
|
||||
t.Errorf("Expected 1 tag after removal, got %d", len(retrieved2.Tags))
|
||||
}
|
||||
|
||||
if retrieved2.Tags[0] != "work" {
|
||||
t.Errorf("Expected remaining tag 'work', got '%s'", retrieved2.Tags[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskComplete(t *testing.T) {
|
||||
task, err := CreateTask("Test complete")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
if err := task.Complete(); err != nil {
|
||||
t.Fatalf("Failed to complete task: %v", err)
|
||||
}
|
||||
|
||||
if task.Status != StatusCompleted {
|
||||
t.Error("Status should be Completed")
|
||||
}
|
||||
|
||||
if task.End == nil {
|
||||
t.Error("End time should be set")
|
||||
}
|
||||
|
||||
// Verify in database
|
||||
retrieved, err := GetTask(task.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get task: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Status != StatusCompleted {
|
||||
t.Error("Retrieved task should be completed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskDelete(t *testing.T) {
|
||||
// Test soft delete
|
||||
task1, err := CreateTask("Test soft delete")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
if err := task1.Delete(false); err != nil {
|
||||
t.Fatalf("Failed to soft delete: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := GetTask(task1.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get deleted task: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Status != StatusDeleted {
|
||||
t.Error("Task should be marked as deleted")
|
||||
}
|
||||
|
||||
// Test hard delete
|
||||
task2, err := CreateTask("Test hard delete")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
if err := task2.Delete(true); err != nil {
|
||||
t.Fatalf("Failed to hard delete: %v", err)
|
||||
}
|
||||
|
||||
_, err = GetTask(task2.UUID)
|
||||
if err == nil {
|
||||
t.Error("Should not be able to retrieve hard-deleted task")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskStartStop(t *testing.T) {
|
||||
task, err := CreateTask("Test start/stop")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
// Start task
|
||||
if err := task.StartTask(); err != nil {
|
||||
t.Fatalf("Failed to start task: %v", err)
|
||||
}
|
||||
|
||||
if task.Start == nil {
|
||||
t.Error("Start time should be set")
|
||||
}
|
||||
|
||||
// Stop task
|
||||
if err := task.StopTask(); err != nil {
|
||||
t.Fatalf("Failed to stop task: %v", err)
|
||||
}
|
||||
|
||||
if task.Start != nil {
|
||||
t.Error("Start time should be nil after stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTasks(t *testing.T) {
|
||||
// Create multiple tasks
|
||||
CreateTask("Task 1")
|
||||
CreateTask("Task 2")
|
||||
CreateTask("Task 3")
|
||||
|
||||
tasks, err := GetTasks()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get tasks: %v", err)
|
||||
}
|
||||
|
||||
if len(tasks) < 3 {
|
||||
t.Errorf("Expected at least 3 tasks, got %d", len(tasks))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskWithDueDate(t *testing.T) {
|
||||
task, err := CreateTask("Task with due date")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create task: %v", err)
|
||||
}
|
||||
|
||||
dueDate := time.Now().Add(24 * time.Hour)
|
||||
task.Due = &dueDate
|
||||
|
||||
if err := task.Save(); err != nil {
|
||||
t.Fatalf("Failed to save task with due date: %v", err)
|
||||
}
|
||||
|
||||
retrieved, err := GetTask(task.UUID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get task: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Due == nil {
|
||||
t.Error("Due date should be set")
|
||||
}
|
||||
|
||||
if retrieved.Due.Unix() != dueDate.Unix() {
|
||||
t.Error("Due dates don't match")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user