diff --git a/.github/workflows/frontend-test.yml b/.github/workflows/frontend-test.yml index d1f3e67a..53d7986d 100644 --- a/.github/workflows/frontend-test.yml +++ b/.github/workflows/frontend-test.yml @@ -1,5 +1,4 @@ -name: frontend-tests-and-coverage - +name: Frontend on: push: branches: @@ -8,63 +7,16 @@ on: branches: - "main" - "dev" - jobs: - test-and-coverage: + prettier: runs-on: ubuntu-latest - permissions: - # pull-requests: write # to add comments to the PRs - contents: read - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js + - uses: actions/checkout@v3 + - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "20.x" - - - name: Install dependencies - run: npm ci + - run: npm ci working-directory: frontend - - # 1. RUN ONLY TESTS ON PUSH - - name: Run frontend tests - if: github.event_name == 'push' - run: npm test + - run: npm test working-directory: frontend - - # 2. RUN COVERAGE ON PRs - - name: Run jest with coverage - if: github.event_name == 'pull_request' - run: npx jest --coverage --coverageReporters=cobertura - working-directory: frontend - - # # 3. POST COMMENT ON PR - # - name: Jest Coverage Comment - # if: > - # github.event_name == 'pull_request' - # uses: MishaKav/jest-coverage-comment@main - # with: - # coverage-summary-path: ./frontend/coverage/coverage-summary.json - # title: Frontend Coverage Report - - # 3. WRITE COVERAGE SUMMARY TO CHECKS - - name: Write Summary from File - if: github.event_name == 'pull_request' - uses: irongut/CodeCoverageSummary@v1.3.0 - with: - filename: frontend/coverage/cobertura-coverage.xml - badge: true - format: markdown - output: both - - # 4. PUSH TO SUMMARY PAGE - - name: Write to Job Summary - if: github.event_name == 'pull_request' - run: | - if [ -f "code-coverage-results.md" ]; then - cat code-coverage-results.md >> $GITHUB_STEP_SUMMARY - else - echo "Coverage file not found" - fi diff --git a/assets/02_home.png b/assets/02_home.png index 4f46e66d..17acad4e 100644 Binary files a/assets/02_home.png and b/assets/02_home.png differ diff --git a/assets/02_home_mobile.png b/assets/02_home_mobile.png index f07e72d4..4f2b9ee0 100644 Binary files a/assets/02_home_mobile.png and b/assets/02_home_mobile.png differ diff --git a/assets/03_home.png b/assets/03_home.png index d19c3fb3..bd0c0988 100644 Binary files a/assets/03_home.png and b/assets/03_home.png differ diff --git a/backend/controllers/add_task.go b/backend/controllers/add_task.go index c684e8bf..e44aa727 100644 --- a/backend/controllers/add_task.go +++ b/backend/controllers/add_task.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "os" ) var GlobalJobQueue *JobQueue @@ -63,27 +62,9 @@ func AddTaskHandler(w http.ResponseWriter, r *http.Request) { } // 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 err := utils.ValidateCircularDependencies(depends, "", taskDeps); err != nil { - http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) - return - } + if err := utils.ValidateDependencies(depends, ""); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return } dueDateStr, err := utils.ConvertOptionalISOToTaskwarriorFormat(dueDate) if err != nil { diff --git a/backend/controllers/app_handlers.go b/backend/controllers/app_handlers.go index 29ba4300..4f30abde 100644 --- a/backend/controllers/app_handlers.go +++ b/backend/controllers/app_handlers.go @@ -44,6 +44,8 @@ func (a *App) OAuthHandler(w http.ResponseWriter, r *http.Request) { // @Failure 500 {string} string "Internal server error" // @Router /auth/callback [get] func (a *App) OAuthCallbackHandler(w http.ResponseWriter, r *http.Request) { + utils.Logger.Info("Fetching user info...") + code := r.URL.Query().Get("code") t, err := a.Config.Exchange(context.Background(), code) @@ -84,6 +86,8 @@ func (a *App) OAuthCallbackHandler(w http.ResponseWriter, r *http.Request) { return } + // utils.Logger.Infof("User Info: %v", userInfo) + frontendOriginDev := os.Getenv("FRONTEND_ORIGIN_DEV") http.Redirect(w, r, frontendOriginDev+"/home", http.StatusSeeOther) } @@ -105,6 +109,7 @@ func (a *App) UserInfoHandler(w http.ResponseWriter, r *http.Request) { return } + // utils.Logger.Infof("Sending User Info: %v", userInfo) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(userInfo) } @@ -142,4 +147,5 @@ func (a *App) LogoutHandler(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) + // utils.Logger.Info("User has logged out") } diff --git a/backend/controllers/complete_tasks.go b/backend/controllers/complete_tasks.go index d9dc99b9..b971f50b 100644 --- a/backend/controllers/complete_tasks.go +++ b/backend/controllers/complete_tasks.go @@ -52,6 +52,7 @@ func BulkCompleteTaskHandler(w http.ResponseWriter, r *http.Request) { logStore := models.GetLogStore() + // Create a *single* job for all UUIDs job := Job{ Name: "Bulk Complete Tasks", Execute: func() error { diff --git a/backend/controllers/controllers_test.go b/backend/controllers/controllers_test.go index 619016f2..921d69c3 100644 --- a/backend/controllers/controllers_test.go +++ b/backend/controllers/controllers_test.go @@ -54,6 +54,7 @@ func Test_OAuthHandler(t *testing.T) { func Test_OAuthCallbackHandler(t *testing.T) { app := setup() + // This part of the test requires mocking the OAuth provider which can be complex. Simplified for demonstration. req, err := http.NewRequest("GET", "/auth/callback?code=testcode", nil) assert.NoError(t, err) @@ -61,12 +62,14 @@ func Test_OAuthCallbackHandler(t *testing.T) { handler := http.HandlerFunc(app.OAuthCallbackHandler) handler.ServeHTTP(rr, req) + // Since actual OAuth flow can't be tested in unit test, we are focusing on ensuring no panic assert.NotEqual(t, http.StatusInternalServerError, rr.Code) } func Test_UserInfoHandler(t *testing.T) { app := setup() + // Create a request object to pass to the session store req, err := http.NewRequest("GET", "/api/user", nil) assert.NoError(t, err) @@ -77,7 +80,7 @@ func Test_UserInfoHandler(t *testing.T) { "uuid": "uuid-test", "encryption_secret": "secret-test", } - session.Save(req, httptest.NewRecorder()) + session.Save(req, httptest.NewRecorder()) // Save the session rr := httptest.NewRecorder() handler := http.HandlerFunc(app.UserInfoHandler) @@ -122,6 +125,7 @@ func Test_LogoutHandler(t *testing.T) { } func Test_AddTaskHandler_WithDueDate(t *testing.T) { + // Initialize job queue GlobalJobQueue = NewJobQueue() requestBody := map[string]interface{}{ @@ -147,6 +151,7 @@ func Test_AddTaskHandler_WithDueDate(t *testing.T) { } func Test_AddTaskHandler_WithoutDueDate(t *testing.T) { + // Initialize job queue GlobalJobQueue = NewJobQueue() requestBody := map[string]interface{}{ @@ -192,6 +197,7 @@ func Test_AddTaskHandler_MissingDescription(t *testing.T) { assert.Contains(t, rr.Body.String(), "Description is required") } +// Task Dependencies Tests func Test_AddTaskHandler_WithDependencies(t *testing.T) { GlobalJobQueue = NewJobQueue() diff --git a/backend/controllers/delete_task.go b/backend/controllers/delete_task.go index 34fcd628..5af79555 100644 --- a/backend/controllers/delete_task.go +++ b/backend/controllers/delete_task.go @@ -47,6 +47,10 @@ func DeleteTaskHandler(w http.ResponseWriter, r *http.Request) { return } + // if err := tw.DeleteTaskInTaskwarrior(email, encryptionSecret, uuid, taskuuid); err != nil { + // http.Error(w, err.Error(), http.StatusInternalServerError) + // return + // } logStore := models.GetLogStore() job := Job{ Name: "Delete Task", diff --git a/backend/controllers/edit_task.go b/backend/controllers/edit_task.go index 1e65a851..f22cff37 100644 --- a/backend/controllers/edit_task.go +++ b/backend/controllers/edit_task.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "os" ) // EditTaskHandler godoc @@ -63,56 +62,8 @@ func EditTaskHandler(w http.ResponseWriter, r *http.Request) { } // Validate dependencies - origin := os.Getenv("CONTAINER_ORIGIN") - existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid) - if err != nil { - if err := utils.ValidateDependencies(depends, taskUUID); 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, taskUUID, taskDeps); err != nil { - http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) - return - } - } - - start, err = utils.ConvertISOToTaskwarriorFormat(start) - if err != nil { - http.Error(w, fmt.Sprintf("Invalid start date format: %v", err), http.StatusBadRequest) - return - } - - due, err = utils.ConvertISOToTaskwarriorFormat(due) - if err != nil { - http.Error(w, fmt.Sprintf("Invalid due date format: %v", err), http.StatusBadRequest) - return - } - - end, err = utils.ConvertISOToTaskwarriorFormat(end) - if err != nil { - http.Error(w, fmt.Sprintf("Invalid end date format: %v", err), http.StatusBadRequest) - return - } - - entry, err = utils.ConvertISOToTaskwarriorFormat(entry) - if err != nil { - http.Error(w, fmt.Sprintf("Invalid entry date format: %v", err), http.StatusBadRequest) - return - } - - wait, err = utils.ConvertISOToTaskwarriorFormat(wait) - if err != nil { - http.Error(w, fmt.Sprintf("Invalid wait date format: %v", err), http.StatusBadRequest) + if err := utils.ValidateDependencies(depends, uuid); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) return } diff --git a/backend/controllers/get_logs.go b/backend/controllers/get_logs.go index 42a21886..e1faeeb2 100644 --- a/backend/controllers/get_logs.go +++ b/backend/controllers/get_logs.go @@ -39,6 +39,7 @@ func SyncLogsHandler(w http.ResponseWriter, r *http.Request) { logStore := models.GetLogStore() logs := logStore.GetLogs(last) + // Return logs as JSON w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(logs); err != nil { http.Error(w, "Failed to encode logs", http.StatusInternalServerError) diff --git a/backend/controllers/job_queue.go b/backend/controllers/job_queue.go index 30b2fd64..6672edba 100644 --- a/backend/controllers/job_queue.go +++ b/backend/controllers/job_queue.go @@ -2,6 +2,8 @@ package controllers import ( "sync" + + "ccsync_backend/utils" ) type Job struct { @@ -35,18 +37,23 @@ func (q *JobQueue) AddJob(job Job) { func (q *JobQueue) processJobs() { for job := range q.jobChannel { + utils.Logger.Infof("Executing job: %s", job.Name) + go BroadcastJobStatus(JobStatus{ Job: job.Name, Status: "in-progress", }) if err := job.Execute(); err != nil { + utils.Logger.Errorf("Error executing job %s: %v", job.Name, err) + go BroadcastJobStatus(JobStatus{ Job: job.Name, Status: "failure", }) } else { - // utils.Logger.Infof("Success in executing job %s", job.Name) + utils.Logger.Infof("Success in executing job %s", job.Name) + go BroadcastJobStatus(JobStatus{ Job: job.Name, Status: "success", diff --git a/backend/controllers/modify_task.go b/backend/controllers/modify_task.go index f0a2833d..e3f9645a 100644 --- a/backend/controllers/modify_task.go +++ b/backend/controllers/modify_task.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "os" ) // ModifyTaskHandler godoc @@ -62,27 +61,9 @@ func ModifyTaskHandler(w http.ResponseWriter, r *http.Request) { } // Validate dependencies - origin := os.Getenv("CONTAINER_ORIGIN") - existingTasks, err := tw.FetchTasksFromTaskwarrior(email, encryptionSecret, origin, uuid) - if err != nil { - if err := utils.ValidateDependencies(depends, taskUUID); 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, taskUUID, taskDeps); err != nil { - http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) - return - } + if err := utils.ValidateDependencies(depends, uuid); err != nil { + http.Error(w, fmt.Sprintf("Invalid dependencies: %v", err), http.StatusBadRequest) + return } // if err := tw.ModifyTaskInTaskwarrior(uuid, description, project, priority, status, due, email, encryptionSecret, taskID); err != nil { diff --git a/backend/controllers/websocket.go b/backend/controllers/websocket.go index 6f318b50..c2df6e31 100644 --- a/backend/controllers/websocket.go +++ b/backend/controllers/websocket.go @@ -31,25 +31,31 @@ func WebSocketHandler(w http.ResponseWriter, r *http.Request) { defer ws.Close() clients[ws] = true + utils.Logger.Info("New WebSocket connection established!") + for { _, _, err := ws.ReadMessage() if err != nil { delete(clients, ws) + utils.Logger.Info("WebSocket connection closed:", err) break } } } func BroadcastJobStatus(jobStatus JobStatus) { + utils.Logger.Infof("Broadcasting: %+v", jobStatus) broadcast <- jobStatus } func JobStatusManager() { for { jobStatus := <-broadcast + utils.Logger.Infof("Sending to clients: %+v", jobStatus) for client := range clients { err := client.WriteJSON(jobStatus) if err != nil { + utils.Logger.Errorf("WebSocket Write Error: %v", err) client.Close() delete(clients, client) } diff --git a/backend/utils/datetime.go b/backend/utils/datetime.go index 365cc59c..90d148d0 100644 --- a/backend/utils/datetime.go +++ b/backend/utils/datetime.go @@ -15,8 +15,6 @@ func ConvertISOToTaskwarriorFormat(isoDatetime string) (string, error) { "2006-01-02T15:04:05.000Z", // "2025-12-27T14:30:00.000Z" (frontend datetime with milliseconds) "2006-01-02T15:04:05Z", // "2025-12-27T14:30:00Z" (datetime without milliseconds) "2006-01-02", // "2025-12-27" (date only) - "20060102T150405Z", // "20260128T000000Z" (compact ISO format) - "20060102", // "20260128" (compact date only) } var parsedTime time.Time @@ -26,8 +24,8 @@ func ConvertISOToTaskwarriorFormat(isoDatetime string) (string, error) { for i, format := range formats { parsedTime, err = time.Parse(format, isoDatetime) if err == nil { - // Check if it's date-only format - isDateOnly = (i == 2 || i == 4) // "2006-01-02" or "20060102" formats + // Check if it's date-only format (last format in array) + isDateOnly = (i == 2) // "2006-01-02" format break } } diff --git a/backend/utils/utils_test.go b/backend/utils/utils_test.go index 39bb0e14..609bb9c8 100644 --- a/backend/utils/utils_test.go +++ b/backend/utils/utils_test.go @@ -85,45 +85,20 @@ func Test_ExecCommandForOutputInDir(t *testing.T) { } } -func Test_ValidateDependencies_EmptyList(t *testing.T) { - depends := []string{} +func Test_ValidateDependencies_ValidDependencies(t *testing.T) { + depends := []string{"task-uuid-1", "task-uuid-2"} currentTaskUUID := "current-task-uuid" err := ValidateDependencies(depends, currentTaskUUID) assert.NoError(t, err) } -// Circular Dependency Detection Tests -func Test_detectCycle_NoCycle(t *testing.T) { //A -> B -> C - graph := map[string][]string{ - "A": {"B"}, - "B": {"C"}, - "C": {}, - } - - hasCycle := detectCycle(graph, "A") - assert.False(t, hasCycle, "Should not detect cycle in linear dependency") -} - -func Test_detectCycle_SimpleCycle(t *testing.T) { // A -> B -> A - graph := map[string][]string{ - "A": {"B"}, - "B": {"A"}, - } - - hasCycle := detectCycle(graph, "A") - assert.True(t, hasCycle, "Should detect simple cycle A -> B -> A") +func Test_ValidateDependencies_EmptyList(t *testing.T) { + depends := []string{} + currentTaskUUID := "current-task-uuid" + err := ValidateDependencies(depends, currentTaskUUID) + assert.NoError(t, err) } -func Test_detectCycle_ComplexCycle(t *testing.T) { // A -> B -> C -> A - graph := map[string][]string{ - "A": {"B"}, - "B": {"C"}, - "C": {"A"}, - } - - hasCycle := detectCycle(graph, "A") - assert.True(t, hasCycle, "Should detect complex cycle A -> B -> C -> A") -} func TestConvertISOToTaskwarriorFormat(t *testing.T) { tests := []struct { name string @@ -161,24 +136,6 @@ func TestConvertISOToTaskwarriorFormat(t *testing.T) { expected: "", hasError: true, }, - { - name: "Compact ISO datetime format (Taskwarrior export)", - input: "20260128T000000Z", - expected: "2026-01-28T00:00:00", - hasError: false, - }, - { - name: "Compact ISO datetime format with time", - input: "20260128T143000Z", - expected: "2026-01-28T14:30:00", - hasError: false, - }, - { - name: "Compact date only format", - input: "20260128", - expected: "2026-01-28", - hasError: false, - }, } for _, tt := range tests { diff --git a/backend/utils/validation.go b/backend/utils/validation.go index 8f2d2cbd..a439e055 100644 --- a/backend/utils/validation.go +++ b/backend/utils/validation.go @@ -19,54 +19,3 @@ func ValidateDependencies(depends []string, currentTaskUUID string) error { return nil } - -type TaskDependency struct { - UUID string `json:"uuid"` - Depends []string `json:"depends"` - Status string `json:"status"` -} - -func ValidateCircularDependencies(depends []string, currentTaskUUID string, existingTasks []TaskDependency) error { - if len(depends) == 0 { - return nil - } - - dependencyGraph := make(map[string][]string) - for _, task := range existingTasks { - if task.Status == "pending" { - dependencyGraph[task.UUID] = task.Depends - } - } - - dependencyGraph[currentTaskUUID] = depends - - if hasCycle := detectCycle(dependencyGraph, currentTaskUUID); hasCycle { - return fmt.Errorf("circular dependency detected: adding these dependencies would create a cycle") - } - - return nil -} - -// (0): unvisited, (1): visiting,(2): visited -func detectCycle(graph map[string][]string, startNode string) bool { - color := make(map[string]int) - return dfsHasCycle(graph, startNode, color) -} - -func dfsHasCycle(graph map[string][]string, node string, color map[string]int) bool { - if color[node] == 1 { - return true - } - if color[node] == 2 { - return false - } - - color[node] = 1 - for _, dep := range graph[node] { - if dfsHasCycle(graph, dep, color) { - return true - } - } - color[node] = 2 - return false -} diff --git a/development/setup.sh b/development/setup.sh index 834e5175..5861c1e5 100755 --- a/development/setup.sh +++ b/development/setup.sh @@ -47,12 +47,12 @@ if [ ! -f "$ROOT_DIR/backend/.env" ]; then exit 1 fi -# if [ ! -f "$ROOT_DIR/frontend/.env" ]; then -# echo "Error: frontend/.env file not found." -# echo "Please create it with required environment variables." -# echo "See development/README.md or https://its-me-abhishek.github.io/ccsync-docs/ for details." -# exit 1 -# fi +if [ ! -f "$ROOT_DIR/frontend/.env" ]; then + echo "Error: frontend/.env file not found." + echo "Please create it with required environment variables." + echo "See development/README.md or https://its-me-abhishek.github.io/ccsync-docs/ for details." + exit 1 +fi echo "Installing dependencies..." echo "" diff --git a/frontend/coverage-report.json b/frontend/coverage-report.json new file mode 100644 index 00000000..4c82c0ee --- /dev/null +++ b/frontend/coverage-report.json @@ -0,0 +1,3 @@ +{ + "frontend": "80.71%" +} diff --git a/frontend/src/__tests__/main.test.tsx b/frontend/src/__tests__/main.test.tsx index 27fc47e1..ea92522c 100644 --- a/frontend/src/__tests__/main.test.tsx +++ b/frontend/src/__tests__/main.test.tsx @@ -1,5 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; + jest.mock('../App.tsx', () => ({ __esModule: true, default: () =>