diff --git a/backend/controllers/add_task.go b/backend/controllers/add_task.go index c684e8bf..b33eaddd 100644 --- a/backend/controllers/add_task.go +++ b/backend/controllers/add_task.go @@ -6,7 +6,6 @@ import ( "ccsync_backend/utils/tw" "encoding/json" "fmt" - "io" "net/http" "os" ) @@ -26,66 +25,44 @@ var GlobalJobQueue *JobQueue // @Router /add-task [post] func AddTaskHandler(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, fmt.Sprintf("error reading request body: %v", err), http.StatusBadRequest) - return - } - defer r.Body.Close() - // fmt.Printf("Raw request body: %s\n", string(body)) - var requestBody models.AddTaskRequestBody - - err = json.Unmarshal(body, &requestBody) - if err != nil { + if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil { http.Error(w, fmt.Sprintf("error decoding request body: %v", err), http.StatusBadRequest) return } - email := requestBody.Email - encryptionSecret := requestBody.EncryptionSecret - uuid := requestBody.UUID - description := requestBody.Description - project := requestBody.Project - priority := requestBody.Priority - dueDate := requestBody.DueDate - start := requestBody.Start - entryDate := requestBody.EntryDate - waitDate := requestBody.WaitDate - end := requestBody.End - recur := requestBody.Recur - tags := requestBody.Tags - annotations := requestBody.Annotations - depends := requestBody.Depends + defer r.Body.Close() - if description == "" { + if requestBody.Description == "" { http.Error(w, "Description is required, and cannot be empty!", http.StatusBadRequest) return } - // Validate dependencies - origin := os.Getenv("CONTAINER_ORIGIN") - existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid) - if err != nil { - if err := utils.ValidateDependencies(depends, ""); err != nil { - http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) - return - } - } else { - taskDeps := make([]utils.TaskDependency, len(existingTasks)) - for i, task := range existingTasks { - taskDeps[i] = utils.TaskDependency{ - UUID: task.UUID, - Depends: task.Depends, - Status: task.Status, + if len(requestBody.Depends) > 0 { + origin := os.Getenv("CONTAINER_ORIGIN") + existingTasks, err := tw.FetchTasksFromTaskwarrior(requestBody.Email, requestBody.EncryptionSecret, origin, requestBody.UUID) + if err != nil { + if err := utils.ValidateDependencies(requestBody.Depends, ""); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } + } else { + taskDeps := make([]utils.TaskDependency, len(existingTasks)) + for i, task := range existingTasks { + taskDeps[i] = utils.TaskDependency{ + UUID: task.UUID, + Depends: task.Depends, + Status: task.Status, + } } - } - if err := utils.ValidateCircularDependencies(depends, "", taskDeps); err != nil { - http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) - return + if err := utils.ValidateCircularDependencies(requestBody.Depends, "", taskDeps); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return + } } } - dueDateStr, err := utils.ConvertOptionalISOToTaskwarriorFormat(dueDate) + + dueDateStr, err := utils.ConvertOptionalISOToTaskwarriorFormat(requestBody.DueDate) if err != nil { http.Error(w, fmt.Sprintf("Invalid due date format: %v", err), http.StatusBadRequest) return @@ -95,13 +72,13 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) { job := Job{ Name: "Add Task", Execute: func() error { - logStore.AddLog("INFO", fmt.Sprintf("Adding task: %s", description), uuid, "Add Task") - err := tw.AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDateStr, start, entryDate, waitDate, end, recur, tags, annotations, depends) + logStore.AddLog("INFO", fmt.Sprintf("Adding task: %s", requestBody.Description), requestBody.UUID, "Add Task") + err := tw.AddTaskToTaskwarrior(requestBody, dueDateStr) if err != nil { - logStore.AddLog("ERROR", fmt.Sprintf("Failed to add task: %v", err), uuid, "Add Task") + logStore.AddLog("ERROR", fmt.Sprintf("Failed to add task: %v", err), requestBody.UUID, "Add Task") return err } - logStore.AddLog("INFO", fmt.Sprintf("Successfully added task: %s", description), uuid, "Add Task") + logStore.AddLog("INFO", fmt.Sprintf("Successfully added task: %s", requestBody.Description), requestBody.UUID, "Add Task") return nil }, } diff --git a/backend/controllers/controllers_test.go b/backend/controllers/controllers_test.go index 619016f2..fa7feae9 100644 --- a/backend/controllers/controllers_test.go +++ b/backend/controllers/controllers_test.go @@ -266,3 +266,102 @@ func Test_EditTaskHandler_WithDependencies(t *testing.T) { assert.Equal(t, http.StatusAccepted, rr.Code) } + +func Test_AddTaskHandler_MalformedJSON(t *testing.T) { + malformedJSON := []byte(`{"email": "test@example.com", "description": `) + + req, err := http.NewRequest("POST", "/add-task", bytes.NewBuffer(malformedJSON)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + AddTaskHandler(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Contains(t, rr.Body.String(), "error decoding request body") +} + +func Test_AddTaskHandler_NullDependencies(t *testing.T) { + GlobalJobQueue = NewJobQueue() + + requestBody := map[string]interface{}{ + "email": "test@example.com", + "encryptionSecret": "secret", + "UUID": "test-uuid", + "description": "Task with null dependencies", + "project": "TestProject", + "priority": "M", + "depends": nil, + "tags": []string{"test"}, + } + + body, _ := json.Marshal(requestBody) + req, err := http.NewRequest("POST", "/add-task", bytes.NewBuffer(body)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + AddTaskHandler(rr, req) + + assert.Equal(t, http.StatusAccepted, rr.Code) +} + +func Test_AddTaskHandler_InvalidDueDateFormat(t *testing.T) { + GlobalJobQueue = NewJobQueue() + + dueDate := "invalid-date" + requestBody := map[string]interface{}{ + "email": "test@example.com", + "encryptionSecret": "secret", + "UUID": "test-uuid", + "description": "Task with invalid due date", + "due": &dueDate, + } + + body, _ := json.Marshal(requestBody) + req, err := http.NewRequest("POST", "/add-task", bytes.NewBuffer(body)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + AddTaskHandler(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Contains(t, rr.Body.String(), "Invalid due date format") +} + +func Test_AddTaskHandler_WithAnnotations(t *testing.T) { + GlobalJobQueue = NewJobQueue() + + requestBody := map[string]interface{}{ + "email": "test@example.com", + "encryptionSecret": "secret", + "UUID": "test-uuid", + "description": "Task with annotations", + "annotations": []map[string]interface{}{ + {"description": "First annotation"}, + {"description": "Second annotation"}, + }, + } + + body, _ := json.Marshal(requestBody) + req, err := http.NewRequest("POST", "/add-task", bytes.NewBuffer(body)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + AddTaskHandler(rr, req) + + assert.Equal(t, http.StatusAccepted, rr.Code) +} + +func Test_AddTaskHandler_InvalidMethod(t *testing.T) { + req, err := http.NewRequest("GET", "/add-task", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + AddTaskHandler(rr, req) + + assert.Equal(t, http.StatusMethodNotAllowed, rr.Code) + assert.Contains(t, rr.Body.String(), "Invalid request method") +} diff --git a/backend/utils/tw/add_task.go b/backend/utils/tw/add_task.go index fc3c2df4..ae0d974f 100644 --- a/backend/utils/tw/add_task.go +++ b/backend/utils/tw/add_task.go @@ -9,20 +9,19 @@ import ( "strings" ) -// add task to the user's tw client -func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, priority, dueDate, start, entryDate string, waitDate string, end, recur string, tags []string, annotations []models.Annotation, depends []string) error { +func AddTaskToTaskwarrior(req models.AddTaskRequestBody, dueDate string) error { if err := utils.ExecCommand("rm", "-rf", "/root/.task"); err != nil { return fmt.Errorf("error deleting Taskwarrior data: %v", err) } - tempDir, err := os.MkdirTemp("", "taskwarrior-"+email) + tempDir, err := os.MkdirTemp("", "taskwarrior-"+req.Email) if err != nil { return fmt.Errorf("failed to create temporary directory: %v", err) } defer os.RemoveAll(tempDir) origin := os.Getenv("CONTAINER_ORIGIN") - if err := SetTaskwarriorConfig(tempDir, encryptionSecret, origin, uuid); err != nil { + if err := SetTaskwarriorConfig(tempDir, req.EncryptionSecret, origin, req.UUID); err != nil { return err } @@ -30,43 +29,38 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p return err } - cmdArgs := []string{"add", description} - if project != "" { - cmdArgs = append(cmdArgs, "project:"+project) + cmdArgs := []string{"add", req.Description} + if req.Project != "" { + cmdArgs = append(cmdArgs, "project:"+req.Project) } - if priority != "" { - cmdArgs = append(cmdArgs, "priority:"+priority) + if req.Priority != "" { + cmdArgs = append(cmdArgs, "priority:"+req.Priority) } if dueDate != "" { cmdArgs = append(cmdArgs, "due:"+dueDate) } - if start != "" { - cmdArgs = append(cmdArgs, "start:"+start) + if req.Start != "" { + cmdArgs = append(cmdArgs, "start:"+req.Start) } - // Add dependencies to the task - if len(depends) > 0 { - dependsStr := strings.Join(depends, ",") + if len(req.Depends) > 0 { + dependsStr := strings.Join(req.Depends, ",") cmdArgs = append(cmdArgs, "depends:"+dependsStr) } - if entryDate != "" { - cmdArgs = append(cmdArgs, "entry:"+entryDate) + if req.EntryDate != "" { + cmdArgs = append(cmdArgs, "entry:"+req.EntryDate) } - if waitDate != "" { - cmdArgs = append(cmdArgs, "wait:"+waitDate) + if req.WaitDate != "" { + cmdArgs = append(cmdArgs, "wait:"+req.WaitDate) } - if end != "" { - cmdArgs = append(cmdArgs, "end:"+end) + if req.End != "" { + cmdArgs = append(cmdArgs, "end:"+req.End) } - // Note: Taskwarrior requires a due date to be set before recur can be set - // Only add recur if dueDate is also provided - if recur != "" && dueDate != "" { - cmdArgs = append(cmdArgs, "recur:"+recur) + if req.Recur != "" && dueDate != "" { + cmdArgs = append(cmdArgs, "recur:"+req.Recur) } - // Add tags to the task - if len(tags) > 0 { - for _, tag := range tags { + if len(req.Tags) > 0 { + for _, tag := range req.Tags { if tag != "" { - // Ensure tag doesn't contain spaces cleanTag := strings.ReplaceAll(tag, " ", "_") cmdArgs = append(cmdArgs, "+"+cleanTag) } @@ -77,7 +71,7 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p return fmt.Errorf("failed to add task: %v\n %v", err, cmdArgs) } - if len(annotations) > 0 { + if len(req.Annotations) > 0 { output, err := utils.ExecCommandForOutputInDir(tempDir, "task", "export") if err != nil { return fmt.Errorf("failed to export tasks: %v", err) @@ -95,7 +89,7 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p lastTask := tasks[len(tasks)-1] taskID := fmt.Sprintf("%d", lastTask.ID) - for _, annotation := range annotations { + for _, annotation := range req.Annotations { if annotation.Description != "" { annotateArgs := []string{"rc.confirmation=off", taskID, "annotate", annotation.Description} if err := utils.ExecCommandInDir(tempDir, "task", annotateArgs...); err != nil { @@ -105,7 +99,6 @@ func AddTaskToTaskwarrior(email, encryptionSecret, uuid, description, project, p } } - // Sync Taskwarrior again if err := SyncTaskwarrior(tempDir); err != nil { return err } diff --git a/backend/utils/tw/taskwarrior_test.go b/backend/utils/tw/taskwarrior_test.go index fb15013d..1a81451e 100644 --- a/backend/utils/tw/taskwarrior_test.go +++ b/backend/utils/tw/taskwarrior_test.go @@ -43,7 +43,23 @@ func TestExportTasks(t *testing.T) { } func TestAddTaskToTaskwarrior(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03T10:30:00", "2025-03-01", "2025-03-01", "2025-03-01", "2025-03-03", "daily", []string{}, []models.Annotation{{Description: "note"}}, []string{}) + req := models.AddTaskRequestBody{ + Email: "email", + EncryptionSecret: "encryption_secret", + UUID: "clientId", + Description: "description", + Project: "", + Priority: "H", + Start: "2025-03-01", + EntryDate: "2025-03-01", + WaitDate: "2025-03-01", + End: "2025-03-03", + Recur: "daily", + Tags: []string{}, + Annotations: []models.Annotation{{Description: "note"}}, + Depends: []string{}, + } + err := AddTaskToTaskwarrior(req, "2025-03-03T10:30:00") if err != nil { t.Errorf("AddTaskToTaskwarrior failed: %v", err) } else { @@ -52,7 +68,23 @@ func TestAddTaskToTaskwarrior(t *testing.T) { } func TestAddTaskToTaskwarriorWithWaitDate(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03T14:00:00", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-04", "", []string{}, []models.Annotation{}, []string{}) + req := models.AddTaskRequestBody{ + Email: "email", + EncryptionSecret: "encryption_secret", + UUID: "clientId", + Description: "description", + Project: "project", + Priority: "H", + Start: "2025-03-04", + EntryDate: "2025-03-04", + WaitDate: "2025-03-04", + End: "2025-03-04", + Recur: "", + Tags: []string{}, + Annotations: []models.Annotation{}, + Depends: []string{}, + } + err := AddTaskToTaskwarrior(req, "2025-03-03T14:00:00") if err != nil { t.Errorf("AddTaskToTaskwarrior with wait date failed: %v", err) } else { @@ -61,7 +93,23 @@ func TestAddTaskToTaskwarriorWithWaitDate(t *testing.T) { } func TestAddTaskToTaskwarriorWithEntryDate(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-05T16:30:00", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-10", "", []string{}, []models.Annotation{}, []string{}) + req := models.AddTaskRequestBody{ + Email: "email", + EncryptionSecret: "encryption_secret", + UUID: "clientId", + Description: "description", + Project: "project", + Priority: "H", + Start: "2025-03-04", + EntryDate: "2025-03-04", + WaitDate: "2025-03-04", + End: "2025-03-10", + Recur: "", + Tags: []string{}, + Annotations: []models.Annotation{}, + Depends: []string{}, + } + err := AddTaskToTaskwarrior(req, "2025-03-05T16:30:00") if err != nil { t.Errorf("AddTaskToTaskwarrior failed: %v", err) } else { @@ -79,7 +127,23 @@ func TestCompleteTaskInTaskwarrior(t *testing.T) { } func TestAddTaskWithTags(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "", "H", "2025-03-03T15:45:00", "2025-03-01", "2025-03-01", "2025-03-01", "2025-03-03", "daily", []string{"work", "important"}, []models.Annotation{{Description: "note"}}, []string{}) + req := models.AddTaskRequestBody{ + Email: "email", + EncryptionSecret: "encryption_secret", + UUID: "clientId", + Description: "description", + Project: "", + Priority: "H", + Start: "2025-03-01", + EntryDate: "2025-03-01", + WaitDate: "2025-03-01", + End: "2025-03-03", + Recur: "daily", + Tags: []string{"work", "important"}, + Annotations: []models.Annotation{{Description: "note"}}, + Depends: []string{}, + } + err := AddTaskToTaskwarrior(req, "2025-03-03T15:45:00") if err != nil { t.Errorf("AddTaskToTaskwarrior with tags failed: %v", err) } else { @@ -88,7 +152,23 @@ func TestAddTaskWithTags(t *testing.T) { } func TestAddTaskToTaskwarriorWithEntryDateAndTags(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-05T16:00:00", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-10", "", []string{"work", "important"}, []models.Annotation{}, []string{}) + req := models.AddTaskRequestBody{ + Email: "email", + EncryptionSecret: "encryption_secret", + UUID: "clientId", + Description: "description", + Project: "project", + Priority: "H", + Start: "2025-03-04", + EntryDate: "2025-03-04", + WaitDate: "2025-03-04", + End: "2025-03-10", + Recur: "", + Tags: []string{"work", "important"}, + Annotations: []models.Annotation{}, + Depends: []string{}, + } + err := AddTaskToTaskwarrior(req, "2025-03-05T16:00:00") if err != nil { t.Errorf("AddTaskToTaskwarrior with entry date and tags failed: %v", err) } else { @@ -97,7 +177,23 @@ func TestAddTaskToTaskwarriorWithEntryDateAndTags(t *testing.T) { } func TestAddTaskToTaskwarriorWithWaitDateWithTags(t *testing.T) { - err := AddTaskToTaskwarrior("email", "encryption_secret", "clientId", "description", "project", "H", "2025-03-03T14:30:00", "2025-03-04", "2025-03-04", "2025-03-04", "2025-03-04", "", []string{"work", "important"}, []models.Annotation{}, []string{}) + req := models.AddTaskRequestBody{ + Email: "email", + EncryptionSecret: "encryption_secret", + UUID: "clientId", + Description: "description", + Project: "project", + Priority: "H", + Start: "2025-03-04", + EntryDate: "2025-03-04", + WaitDate: "2025-03-04", + End: "2025-03-04", + Recur: "", + Tags: []string{"work", "important"}, + Annotations: []models.Annotation{}, + Depends: []string{}, + } + err := AddTaskToTaskwarrior(req, "2025-03-03T14:30:00") if err != nil { t.Errorf("AddTaskToTaskwarrior with wait date failed: %v", err) } else {