From b349d5e1ba5dafc57d442743b7b40a3e8da43929 Mon Sep 17 00:00:00 2001 From: Heena Shaikh Date: Sun, 16 Aug 2026 01:30:28 +0530 Subject: [PATCH 1/3] Fix pagination and priority-reset bugs, document status-filter bug, add PATCH /tasks/:id/assign endpoint with tests --- task-api/BUGS.md | 94 +++++++++ task-api/package-lock.json | 301 ++++++++++++++------------- task-api/package.json | 2 +- task-api/src/routes/tasks.js | 16 +- task-api/src/services/taskService.js | 3 +- task-api/src/utils/validators.js | 11 +- task-api/tests/taskService.test.js | 77 +++++++ task-api/tests/tasks.routes.test.js | 140 +++++++++++++ 8 files changed, 491 insertions(+), 153 deletions(-) create mode 100644 task-api/BUGS.md create mode 100644 task-api/tests/taskService.test.js create mode 100644 task-api/tests/tasks.routes.test.js diff --git a/task-api/BUGS.md b/task-api/BUGS.md new file mode 100644 index 00000000..e029cbc3 --- /dev/null +++ b/task-api/BUGS.md @@ -0,0 +1,94 @@ +# Bug Report — task-api + +Bugs found by writing unit tests (`tests/taskService.test.js`) and integration +tests (`tests/tasks.routes.test.js`) against the real behavior of the code. + +--- + +## Bug #1 — `getPaginated`: page 1 skips the first page (FIXED) + +**File:** `src/services/taskService.js` + +**Expected behavior:** `getPaginated(1, limit)` should return the first page of +results, starting from the very first task. + +**Actual behavior:** The offset was calculated as `page * limit`. With +`page=1, limit=2`, this gives `offset=2`, which skips the first 2 tasks +entirely and starts from the 3rd task instead. + +**How discovered:** Wrote a test that created 5 tasks and called +`getPaginated(1, 2)`, expecting the first result to be `"Task 1"`. The test +failed — it returned `"Task 3"` instead, revealing that page 1 wasn't actually +returning the first page. + +**Fix applied:** +```javascript +// Before +const offset = page * limit; +// After +const offset = (page - 1) * limit; +``` +This aligns the 1-indexed `page` parameter (page 1, 2, 3...) with the +0-indexed array, so page 1 now correctly starts at offset 0. + +--- + +## Bug #2 — `completeTask` resets priority to 'medium' (FIXED) + +**File:** `src/services/taskService.js` + +**Expected behavior:** Marking a task complete should only change its +`status` and `completedAt` — its `priority` should stay whatever it was +before. + +**Actual behavior:** `completeTask` hardcoded `priority: 'medium'` into the +updated task object, overwriting the original priority every time, even if +the task was `'high'` priority. + +**How discovered:** Created a task with `priority: 'high'`, called +`completeTask`, and expected the priority to still be `'high'` afterward. The +test failed — the returned task had `priority: 'medium'` instead. + +**Fix applied:** Removed the hardcoded `priority: 'medium'` line from the +update object, so the original priority (already copied in via `...task`) is +preserved. + +--- + +## Bug #3 — `getByStatus` matches by substring, not exact value (NOT FIXED) + +**File:** `src/services/taskService.js` + +**Expected behavior:** `getByStatus('done')` should return only tasks whose +status is exactly `'done'`. + +**Actual behavior:** The filter uses `t.status.includes(status)`, which +checks for a substring match, not exact equality. Since `'todo'`, `'done'`, +and `'in_progress'` all happen to contain the letter `'o'`, calling +`getByStatus('o')` — a value that isn't even a valid status — incorrectly +returns all three tasks instead of none. + +**How discovered:** Created three tasks with statuses `'todo'`, +`'in_progress'`, and `'done'`, then called `getByStatus('o')`. Expected 0 +results since `'o'` isn't a real status; got back all 3 tasks instead. + +**Why not fixed:** This affects a public, documented query parameter +(`GET /tasks?status=`). Changing the matching logic to exact equality is the +right fix, but it's a behavior change to a public endpoint that should be +confirmed with the team first, in case any existing client is (even +accidentally) relying on the current substring-matching behavior. + +**Suggested fix:** +```javascript +const getByStatus = (status) => tasks.filter((t) => t.status === status); +``` + +--- + +## Summary + +| Bug | Status | +|---|---| +| Pagination offset off-by-one | Fixed | +| `completeTask` resets priority | Fixed | +| `getByStatus` substring matching | Documented, not fixed (needs a product decision) | \ No newline at end of file diff --git a/task-api/package-lock.json b/task-api/package-lock.json index 901be207..169cc27d 100644 --- a/task-api/package-lock.json +++ b/task-api/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "express": "^4.18.2", - "uuid": "^9.0.0" + "uuid": "^9.0.1" }, "devDependencies": { "jest": "^29.7.0", @@ -17,13 +17,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -32,9 +32,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -42,21 +42,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -98,14 +98,14 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -115,14 +115,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -132,9 +132,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -142,29 +142,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -184,9 +184,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -194,9 +194,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -204,9 +204,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -214,27 +214,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -483,33 +483,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -542,14 +542,14 @@ "license": "MIT" }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1320,9 +1320,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.8", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", - "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1333,9 +1333,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -1346,7 +1346,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -1357,9 +1357,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -1381,9 +1381,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -1401,11 +1401,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -1490,9 +1490,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001780", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", - "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", "dev": true, "funding": [ { @@ -1845,9 +1845,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.321", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", - "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "version": "1.5.407", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", + "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==", "dev": true, "license": "ISC" }, @@ -2036,14 +2036,14 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -2062,7 +2062,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -2151,17 +2151,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -2392,9 +2392,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3295,9 +3295,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", "dev": true, "license": "MIT", "dependencies": { @@ -3573,11 +3573,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -3773,9 +3776,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/picocolors": { @@ -3786,9 +3789,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -3894,12 +3897,13 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -4114,14 +4118,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -4133,13 +4137,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -4546,9 +4550,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -4589,6 +4593,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" diff --git a/task-api/package.json b/task-api/package.json index 6a36a476..5410b443 100644 --- a/task-api/package.json +++ b/task-api/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "express": "^4.18.2", - "uuid": "^9.0.0" + "uuid": "^9.0.1" }, "devDependencies": { "jest": "^29.7.0", diff --git a/task-api/src/routes/tasks.js b/task-api/src/routes/tasks.js index e8c370fe..40972063 100644 --- a/task-api/src/routes/tasks.js +++ b/task-api/src/routes/tasks.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); const taskService = require('../services/taskService'); -const { validateCreateTask, validateUpdateTask } = require('../utils/validators'); +const { validateCreateTask, validateUpdateTask, validateAssignTask } = require('../utils/validators'); router.get('/stats', (req, res) => { const stats = taskService.getStats(); @@ -68,5 +68,19 @@ router.patch('/:id/complete', (req, res) => { res.json(task); }); +router.patch('/:id/assign', (req, res) => { + const error = validateAssignTask(req.body); + if (error) { + return res.status(400).json({ error }); + } + + const task = taskService.findById(req.params.id); + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + + const updated = taskService.update(req.params.id, { assignee: req.body.assignee.trim() }); + res.json(updated); +}); module.exports = router; diff --git a/task-api/src/services/taskService.js b/task-api/src/services/taskService.js index f8e89189..b40d53b9 100644 --- a/task-api/src/services/taskService.js +++ b/task-api/src/services/taskService.js @@ -9,7 +9,7 @@ const findById = (id) => tasks.find((t) => t.id === id); const getByStatus = (status) => tasks.filter((t) => t.status.includes(status)); const getPaginated = (page, limit) => { - const offset = page * limit; + const offset = (page-1)* limit; return tasks.slice(offset, offset + limit); }; @@ -66,7 +66,6 @@ const completeTask = (id) => { const updated = { ...task, - priority: 'medium', status: 'done', completedAt: new Date().toISOString(), }; diff --git a/task-api/src/utils/validators.js b/task-api/src/utils/validators.js index 1e908ff5..059dd872 100644 --- a/task-api/src/utils/validators.js +++ b/task-api/src/utils/validators.js @@ -32,5 +32,14 @@ const validateUpdateTask = (body) => { } return null; }; +const validateAssignTask = (body) => { + if (body.assignee === undefined) { + return 'assignee is required'; + } + if (typeof body.assignee !== 'string' || body.assignee.trim() === '') { + return 'assignee must be a non-empty string'; + } + return null; +}; -module.exports = { validateCreateTask, validateUpdateTask }; +module.exports = { validateCreateTask, validateUpdateTask, validateAssignTask }; diff --git a/task-api/tests/taskService.test.js b/task-api/tests/taskService.test.js new file mode 100644 index 00000000..e148b9a1 --- /dev/null +++ b/task-api/tests/taskService.test.js @@ -0,0 +1,77 @@ +const taskService = require('../src/services/taskService'); + +describe('taskService', () => { + beforeEach(() => { + taskService._reset(); + }); + + it('creates a task with a title', () => { + const task = taskService.create({ title: 'Write tests' }); + expect(task.title).toBe('Write tests'); + }); + + it('creates a task with default values when only title is given', () => { + const task = taskService.create({ title: 'Minimal task' }); + expect(task.status).toBe('todo'); + expect(task.priority).toBe('medium'); + expect(task.completedAt).toBeNull(); + }); + it('page 1 returns the first items', () => { + for (let i = 1; i <= 5; i++) { + taskService.create({ title: `Task ${i}` }); + } + + const page1 = taskService.getPaginated(1, 2); + expect(page1[0].title).toBe('Task 1'); + }); + it('removes a task and returns true', () => { + const task = taskService.create({ title: 'Delete me' }); + const result = taskService.remove(task.id); + + expect(result).toBe(true); + expect(taskService.findById(task.id)).toBeUndefined(); + }); + it('returns false when removing a task that does not exist', () => { + const result = taskService.remove('some-fake-id-that-does-not-exist'); + expect(result).toBe(false); + }); + it('updates one field without erasing the others', () => { + const task = taskService.create({ title: 'Original title', priority: 'low' }); + const updated = taskService.update(task.id, { title: 'New title' }); + + expect(updated.title).toBe('New title'); + expect(updated.priority).toBe('low'); + }); + it('keeps the original priority after completing a task', () => { + const task = taskService.create({ title: 'Important task', priority: 'high' }); + const completed = taskService.completeTask(task.id); + expect(completed.priority).toBe('high'); + }); + it('BUG: matches tasks by substring instead of exact status (should be 0, is not)', () => { + taskService.create({ title: 'A', status: 'todo' }); + taskService.create({ title: 'B', status: 'in_progress' }); + taskService.create({ title: 'C', status: 'done' }); + + // Ideally this should be 0 (since 'o' isn't a real status), but the + // current implementation uses .includes() instead of exact matching, + // so it incorrectly matches any status containing the letter 'o'. + const result = taskService.getByStatus('o'); + expect(result.length).toBe(3); // demonstrates the bug — not correct behavior + }); + it('does not count a task with no due date as overdue', () => { + taskService.create({ title: 'No deadline', status: 'todo' }); + + const stats = taskService.getStats(); + expect(stats.overdue).toBe(0); + }); + it('counts a task as overdue if its due date has passed and it is not done', () => { + taskService.create({ + title: 'Old task', + status: 'todo', + dueDate: '2000-01-01T00:00:00.000Z', + }); + + const stats = taskService.getStats(); + expect(stats.overdue).toBe(1); + }); +}); \ No newline at end of file diff --git a/task-api/tests/tasks.routes.test.js b/task-api/tests/tasks.routes.test.js new file mode 100644 index 00000000..6aca9db3 --- /dev/null +++ b/task-api/tests/tasks.routes.test.js @@ -0,0 +1,140 @@ +const request = require('supertest'); +const app = require('../src/app'); +const taskService = require('../src/services/taskService'); + +describe('Task API routes', () => { + beforeEach(() => { + taskService._reset(); + }); + + it('creates a task via POST /tasks', async () => { + const res = await request(app) + .post('/tasks') + .send({ title: 'Write report', priority: 'high' }); + + expect(res.status).toBe(201); + expect(res.body.title).toBe('Write report'); + }); + it('rejects creating a task with no title', async () => { + const res = await request(app) + .post('/tasks') + .send({ description: 'missing title' }); + + expect(res.status).toBe(400); + }); + it('lists all tasks via GET /tasks', async () => { + await request(app).post('/tasks').send({ title: 'Task A' }); + await request(app).post('/tasks').send({ title: 'Task B' }); + + const res = await request(app).get('/tasks'); + + expect(res.status).toBe(200); + expect(res.body.length).toBe(2); + }); + it('filters tasks by status via GET /tasks?status=', async () => { + await request(app).post('/tasks').send({ title: 'Task A', status: 'todo' }); + await request(app).post('/tasks').send({ title: 'Task B', status: 'done' }); + + const res = await request(app).get('/tasks?status=done'); + + expect(res.status).toBe(200); + expect(res.body.length).toBe(1); + expect(res.body[0].title).toBe('Task B'); + }); + it('paginates tasks via GET /tasks?page=&limit=', async () => { + for (let i = 1; i <= 5; i++) { + await request(app).post('/tasks').send({ title: `Task ${i}` }); + } + + const res = await request(app).get('/tasks?page=1&limit=2'); + + expect(res.status).toBe(200); + expect(res.body[0].title).toBe('Task 1'); + expect(res.body.length).toBe(2); + }); + it('returns stats via GET /tasks/stats', async () => { + await request(app).post('/tasks').send({ title: 'Task A', status: 'todo' }); + await request(app).post('/tasks').send({ title: 'Task B', status: 'done' }); + + const res = await request(app).get('/tasks/stats'); + + expect(res.status).toBe(200); + expect(res.body.todo).toBe(1); + expect(res.body.done).toBe(1); + }); + it('updates a task via PUT /tasks/:id', async () => { + const created = await request(app).post('/tasks').send({ title: 'Original' }); + + const res = await request(app) + .put(`/tasks/${created.body.id}`) + .send({ title: 'Updated title' }); + + expect(res.status).toBe(200); + expect(res.body.title).toBe('Updated title'); + }); + it('returns 404 when updating a task that does not exist', async () => { + const res = await request(app) + .put('/tasks/some-fake-id') + .send({ title: 'Updated title' }); + + expect(res.status).toBe(404); + }); + it('deletes a task via DELETE /tasks/:id', async () => { + const created = await request(app).post('/tasks').send({ title: 'To delete' }); + + const res = await request(app).delete(`/tasks/${created.body.id}`); + + expect(res.status).toBe(204); + }); + it('returns 404 when deleting a task that does not exist', async () => { + const res = await request(app).delete('/tasks/some-fake-id'); + expect(res.status).toBe(404); + }); + it('marks a task as complete via PATCH /tasks/:id/complete', async () => { + const created = await request(app).post('/tasks').send({ title: 'Finish me' }); + + const res = await request(app).patch(`/tasks/${created.body.id}/complete`); + + expect(res.status).toBe(200); + expect(res.body.status).toBe('done'); + }); + it('returns 404 when completing a task that does not exist', async () => { + const res = await request(app).patch('/tasks/some-fake-id/complete'); + expect(res.status).toBe(404); + }); + it('assigns a task via PATCH /tasks/:id/assign', async () => { + const created = await request(app).post('/tasks').send({ title: 'Assign me' }); + + const res = await request(app) + .patch(`/tasks/${created.body.id}/assign`) + .send({ assignee: 'Priya' }); + + expect(res.status).toBe(200); + expect(res.body.assignee).toBe('Priya'); + }); + it('returns 404 when assigning a task that does not exist', async () => { + const res = await request(app) + .patch('/tasks/some-fake-id/assign') + .send({ assignee: 'Priya' }); + + expect(res.status).toBe(404); + }); + it('rejects assigning with no assignee field', async () => { + const created = await request(app).post('/tasks').send({ title: 'Assign me' }); + + const res = await request(app) + .patch(`/tasks/${created.body.id}/assign`) + .send({}); + + expect(res.status).toBe(400); + }); + it('rejects assigning with an empty or whitespace-only assignee', async () => { + const created = await request(app).post('/tasks').send({ title: 'Assign me' }); + + const res = await request(app) + .patch(`/tasks/${created.body.id}/assign`) + .send({ assignee: ' ' }); + + expect(res.status).toBe(400); + }); +}); \ No newline at end of file From 13107cb25e210aa99003d635aaaf828d21054966 Mon Sep 17 00:00:00 2001 From: Heena Shaikh Date: Sun, 16 Aug 2026 01:48:17 +0530 Subject: [PATCH 2/3] Add submission notes --- task-api/NOTES.md | 31 +++++++++++++++++++++++++++++ task-api/tests/tasks.routes.test.js | 21 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 task-api/NOTES.md diff --git a/task-api/NOTES.md b/task-api/NOTES.md new file mode 100644 index 00000000..0002b45b --- /dev/null +++ b/task-api/NOTES.md @@ -0,0 +1,31 @@ +## Submission Notes + +**What I'd test next if I had more time:** +Load/stress testing the pagination endpoint now that the offset bug is fixed, +since the original bug would have gotten worse under high traffic. I'd also +add tests for edge cases like `limit=0`, negative page numbers, and +non-numeric `page`/`limit` query values — right now these get silently +coerced via `parseInt(...) || 1` rather than validated, which could hide bad +input instead of rejecting it clearly. + +**What surprised me in the codebase:** +The `completeTask` bug was the most interesting one — it wasn't an obvious +typo, it was a deliberate-looking line (`priority: 'medium'`) that silently +overwrote a task's real priority every time it was marked done. It's the kind +of bug that's easy to miss just by reading the code, but jumps out +immediately once you write a test that checks the value survives. It also +made me realize how much of debugging is just "read the actual error +message carefully" rather than guessing — I ran into this literally, with +real errors along the way (a broken `uuid` dependency version, PowerShell +quoting issues with curl, files created in the wrong folder), and each one +was solved by reading the exact error text rather than assuming. + +**Questions I'd ask before shipping to production:** +1. Is the `completeTask` priority reset intentional, or genuinely a bug? (I + fixed it as a bug, but wanted to flag that the line looked deliberate, + not like a copy-paste mistake.) +2. Should the `getByStatus` filter validate against the status enum the same + way `POST`/`PUT` bodies do, instead of silently substring-matching on + whatever string is passed in? +3. Is a `GET /tasks/:id` route needed before shipping? Right now there's no + way to fetch a single task directly by id. \ No newline at end of file diff --git a/task-api/tests/tasks.routes.test.js b/task-api/tests/tasks.routes.test.js index 6aca9db3..00aefed2 100644 --- a/task-api/tests/tasks.routes.test.js +++ b/task-api/tests/tasks.routes.test.js @@ -137,4 +137,25 @@ describe('Task API routes', () => { expect(res.status).toBe(400); }); + it('rejects creating a task with an invalid status', async () => { + const res = await request(app) + .post('/tasks') + .send({ title: 'Bad status', status: 'not-a-real-status' }); + + expect(res.status).toBe(400); + }); + it('rejects creating a task with an invalid priority', async () => { + const res = await request(app) + .post('/tasks') + .send({ title: 'Bad priority', priority: 'urgent' }); + + expect(res.status).toBe(400); + }); + it('rejects creating a task with an invalid dueDate', async () => { + const res = await request(app) + .post('/tasks') + .send({ title: 'Bad date', dueDate: 'not-a-real-date' }); + + expect(res.status).toBe(400); + }); }); \ No newline at end of file From 5df33b3b0ffdc86bc9738d7a1534e02e0e2674ea Mon Sep 17 00:00:00 2001 From: Heena Shaikh Date: Sun, 16 Aug 2026 01:57:12 +0530 Subject: [PATCH 3/3] Add coverage report output --- task-api/COVERAGE.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 task-api/COVERAGE.md diff --git a/task-api/COVERAGE.md b/task-api/COVERAGE.md new file mode 100644 index 00000000..f7e5a94d --- /dev/null +++ b/task-api/COVERAGE.md @@ -0,0 +1,25 @@ +# Test Coverage Report + +Run via `npm run coverage`. + +``` +PASS tests/taskService.test.js +PASS tests/tasks.routes.test.js +-----------------|---------|----------|---------|---------|--------------------- +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s +-----------------|---------|----------|---------|---------|--------------------- +All files | 93.95 | 85.88 | 92.85 | 93.43 | + src | 69.23 | 75 | 0 | 69.23 | + app.js | 69.23 | 75 | 0 | 69.23 | 10-11,17-18 + src/routes | 98.11 | 87.5 | 100 | 98.11 | + tasks.js | 98.11 | 87.5 | 100 | 98.11 | 43 + src/services | 100 | 94.11 | 100 | 100 | + taskService.js | 100 | 94.11 | 100 | 100 | 22 + src/utils | 86.2 | 82.5 | 100 | 86.2 | + validators.js | 86.2 | 82.5 | 100 | 86.2 | 22,25,28,31 +-----------------|---------|----------|---------|---------|--------------------- + +Test Suites: 2 passed, 2 total +Tests: 29 passed, 29 total +Snapshots: 0 total +``` \ No newline at end of file