From 192409134fa60ebda8aebe7648dcac0b9ba2bbc9 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 02:47:47 +0530 Subject: [PATCH 01/12] create tests for get and post methods --- task-api/tests/createTasks.test.js | 88 +++++++++++++++++++++++++ task-api/tests/getTasks.test.js | 102 +++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 task-api/tests/createTasks.test.js create mode 100644 task-api/tests/getTasks.test.js diff --git a/task-api/tests/createTasks.test.js b/task-api/tests/createTasks.test.js new file mode 100644 index 00000000..448fa807 --- /dev/null +++ b/task-api/tests/createTasks.test.js @@ -0,0 +1,88 @@ +const request = require('supertest'); +const app = require('../app'); + +describe('POST /tasks', () => { + + const validTask = { + title: 'Complete assignment', + description: 'Write API tests', + status: 'todo', + priority: 'high', + dueDate: new Date().toISOString() + }; + + it('should create a new task successfully', async () => { + const res = await request(app) + .post('/tasks') + .send(validTask); + + expect(res.statusCode).toBe(201); + + expect(res.body).toHaveProperty('id'); + expect(res.body.title).toBe(validTask.title); + expect(res.body.description).toBe(validTask.description); + expect(res.body.status).toBe(validTask.status); + expect(res.body.priority).toBe(validTask.priority); + + expect(res.body.createdAt).toBeDefined(); + + expect(res.body.completedAt).toBeNull(); + }); + + it('should fail if title is missing', async () => { + const res = await request(app) + .post('/tasks') + .send({ + description: 'No title task', + status: 'todo', + priority: 'low' + }); + + expect(res.statusCode).toBe(400); + }); + + it('should fail for invalid status value', async () => { + const res = await request(app) + .post('/tasks') + .send({ + ...validTask, + status: 'invalid_status' + }); + + expect(res.statusCode).toBe(400); + }); + + it('should fail for invalid priority value', async () => { + const res = await request(app) + .post('/tasks') + .send({ + ...validTask, + priority: 'urgent' + }); + + expect(res.statusCode).toBe(400); + }); + + it('should fail for invalid dueDate format', async () => { + const res = await request(app) + .post('/tasks') + .send({ + ...validTask, + dueDate: 'not-a-date' + }); + + expect(res.statusCode).toBe(400); + }); + + it('should ignore or reject extra fields', async () => { + const res = await request(app) + .post('/tasks') + .send({ + ...validTask, + randomField: 'hack' + }); + + expect([200, 201, 400]).toContain(res.statusCode); + }); + +}); \ No newline at end of file diff --git a/task-api/tests/getTasks.test.js b/task-api/tests/getTasks.test.js new file mode 100644 index 00000000..b0c7fc2d --- /dev/null +++ b/task-api/tests/getTasks.test.js @@ -0,0 +1,102 @@ +const request = require('supertest'); +const app = require('../app'); + +describe('GET /tasks?status=todo', () => { + + beforeEach(async () => { + await request(app).post('/tasks').send({ + title: 'Task 1', + description: 'Todo task', + status: 'todo', + priority: 'low' + }); + + await request(app).post('/tasks').send({ + title: 'Task 2', + description: 'In progress task', + status: 'in_progress', + priority: 'medium' + }); + + await request(app).post('/tasks').send({ + title: 'Task 3', + description: 'Another todo', + status: 'todo', + priority: 'high' + }); + }); + + it('should return only tasks with status todo', async () => { + const res = await request(app).get('/tasks?status=todo'); + + expect(res.statusCode).toBe(200); + + expect(Array.isArray(res.body)).toBe(true); + + res.body.forEach(task => { + expect(task.status).toBe('todo'); + }); + + expect(res.body.length).toBeGreaterThan(0); + }); + + it('should return 400 for invalid status query', async () => { + const res = await request(app).get('/tasks?status=invalid'); + + expect(res.statusCode).toBe(400); + }); + +}); + + + + +describe('GET /tasks?page=1&limit=10', () => { + + beforeEach(async () => { + for (let i = 1; i <= 15; i++) { + await request(app).post('/tasks').send({ + title: `Task ${i}`, + description: `Task number ${i}`, + status: 'todo', + priority: 'low' + }); + } + }); + + it('should return paginated tasks with correct limit', async () => { + const res = await request(app).get('/tasks?page=1&limit=10'); + + expect(res.statusCode).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + + expect(res.body.length).toBeLessThanOrEqual(10); + }); + + it('should return different tasks for page 2', async () => { + const page1 = await request(app).get('/tasks?page=1&limit=10'); + const page2 = await request(app).get('/tasks?page=2&limit=10'); + + expect(page1.statusCode).toBe(200); + expect(page2.statusCode).toBe(200); + + const idsPage1 = page1.body.map(t => t.id); + const idsPage2 = page2.body.map(t => t.id); + + const overlap = idsPage1.some(id => idsPage2.includes(id)); + expect(overlap).toBe(false); + }); + + it('should return 400 for invalid page value', async () => { + const res = await request(app).get('/tasks?page=-1&limit=10'); + + expect(res.statusCode).toBe(400); + }); + + it('should return 400 for invalid limit value', async () => { + const res = await request(app).get('/tasks?page=1&limit=0'); + + expect(res.statusCode).toBe(400); + }); + +}); \ No newline at end of file From d696e0fea48d45fbc56b79772bb6c3ab9300fb89 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 03:09:44 +0530 Subject: [PATCH 02/12] wrote update tests --- task-api/tests/getTasks.test.js | 74 ++++++++++++++++++++++++++ task-api/tests/updateTasks.test.js | 83 ++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 task-api/tests/updateTasks.test.js diff --git a/task-api/tests/getTasks.test.js b/task-api/tests/getTasks.test.js index b0c7fc2d..4fca3a6f 100644 --- a/task-api/tests/getTasks.test.js +++ b/task-api/tests/getTasks.test.js @@ -99,4 +99,78 @@ describe('GET /tasks?page=1&limit=10', () => { expect(res.statusCode).toBe(400); }); +}); + + + + +describe('GET /tasks/stats', () => { + + beforeEach(async () => { + const now = new Date(); + const pastDate = new Date(now.getTime() - 86400000).toISOString(); // yesterday + const futureDate = new Date(now.getTime() + 86400000).toISOString(); // tomorrow + + await request(app).post('/tasks').send({ + title: 'Todo Task', + description: 'Task 1', + status: 'todo', + priority: 'low', + dueDate: futureDate + }); + + await request(app).post('/tasks').send({ + title: 'In Progress Task', + description: 'Task 2', + status: 'in_progress', + priority: 'medium', + dueDate: futureDate + }); + + await request(app).post('/tasks').send({ + title: 'Done Task', + description: 'Task 3', + status: 'done', + priority: 'high', + dueDate: pastDate + }); + + await request(app).post('/tasks').send({ + title: 'Overdue Task', + description: 'Task 4', + status: 'todo', + priority: 'high', + dueDate: pastDate + }); + }); + + it('should return correct task statistics', async () => { + const res = await request(app).get('/tasks/stats'); + + expect(res.statusCode).toBe(200); + + expect(res.body).toHaveProperty('todo'); + expect(res.body).toHaveProperty('in_progress'); + expect(res.body).toHaveProperty('done'); + expect(res.body).toHaveProperty('overdue'); + + expect(res.body.todo).toBe(2); + expect(res.body.in_progress).toBe(1); + expect(res.body.done).toBe(1); + expect(res.body.overdue).toBe(1); + }); + + it('should return zero counts when no tasks exist', async () => { + const res = await request(app).get('/tasks/stats'); + + expect(res.statusCode).toBe(200); + + expect(res.body).toEqual({ + todo: 0, + in_progress: 0, + done: 0, + overdue: 0 + }); + }); + }); \ No newline at end of file diff --git a/task-api/tests/updateTasks.test.js b/task-api/tests/updateTasks.test.js new file mode 100644 index 00000000..ba183bf6 --- /dev/null +++ b/task-api/tests/updateTasks.test.js @@ -0,0 +1,83 @@ +const request = require('supertest'); +const app = require('../app'); + +describe('PUT /tasks/:id', () => { + + let taskId; + + beforeEach(async () => { + const res = await request(app).post('/tasks').send({ + title: 'Original Task', + description: 'Before update', + status: 'todo', + priority: 'low' + }); + + taskId = res.body.id; + }); + + it('should update a task successfully', async () => { + const updatedData = { + title: 'Updated Task', + status: 'in_progress', + priority: 'high' + }; + + const res = await request(app) + .put(`/tasks/${taskId}`) + .send(updatedData); + + expect(res.statusCode).toBe(200); + + expect(res.body.id).toBe(taskId); + expect(res.body.title).toBe(updatedData.title); + expect(res.body.status).toBe(updatedData.status); + expect(res.body.priority).toBe(updatedData.priority); + }); + + it('should return 404 for non-existing task', async () => { + const fakeId = '123e4567-e89b-12d3-a456-426614174000'; + + const res = await request(app) + .put(`/tasks/${fakeId}`) + .send({ title: 'Does not exist' }); + + expect(res.statusCode).toBe(404); + }); + + it('should return 400 for invalid status', async () => { + const res = await request(app) + .put(`/tasks/${taskId}`) + .send({ status: 'wrong_status' }); + + expect(res.statusCode).toBe(400); + }); + + it('should return 400 for invalid priority', async () => { + const res = await request(app) + .put(`/tasks/${taskId}`) + .send({ priority: 'urgent' }); + + expect(res.statusCode).toBe(400); + }); + + it('should return 400 for invalid dueDate format', async () => { + const res = await request(app) + .put(`/tasks/${taskId}`) + .send({ dueDate: 'invalid-date' }); + + expect(res.statusCode).toBe(400); + }); + + it('should update only provided fields (partial update)', async () => { + const res = await request(app) + .put(`/tasks/${taskId}`) + .send({ status: 'done' }); + + expect(res.statusCode).toBe(200); + expect(res.body.status).toBe('done'); + + expect(res.body.title).toBeDefined(); + }); + +}); \ No newline at end of file From 8da04346d7818bbae4c53e63dab0003a968b1dd1 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 03:14:36 +0530 Subject: [PATCH 03/12] add delete amd patch tests --- task-api/tests/deleteTasks.test.js | 41 ++++++++++++++++++++++++++ task-api/tests/updateTasks.test.js | 47 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 task-api/tests/deleteTasks.test.js diff --git a/task-api/tests/deleteTasks.test.js b/task-api/tests/deleteTasks.test.js new file mode 100644 index 00000000..81befd88 --- /dev/null +++ b/task-api/tests/deleteTasks.test.js @@ -0,0 +1,41 @@ +const request = require('supertest'); +const app = require('../app'); + +describe('DELETE /tasks/:id', () => { + + let taskId; + + beforeEach(async () => { + const res = await request(app).post('/tasks').send({ + title: 'Task to delete', + description: 'Delete me', + status: 'todo', + priority: 'low' + }); + + taskId = res.body.id; + }); + + it('should delete a task successfully', async () => { + const res = await request(app).delete(`/tasks/${taskId}`); + + expect([200, 204]).toContain(res.statusCode); + }); + + it('should not find the task after deletion', async () => { + await request(app).delete(`/tasks/${taskId}`); + + const res = await request(app).get(`/tasks/${taskId}`); + + expect(res.statusCode).toBe(404); + }); + + it('should return 404 for non-existing task', async () => { + const fakeId = '123e4567-e89b-12d3-a456-426614174000'; + + const res = await request(app).delete(`/tasks/${fakeId}`); + + expect(res.statusCode).toBe(404); + }); + +}); \ No newline at end of file diff --git a/task-api/tests/updateTasks.test.js b/task-api/tests/updateTasks.test.js index ba183bf6..28427783 100644 --- a/task-api/tests/updateTasks.test.js +++ b/task-api/tests/updateTasks.test.js @@ -80,4 +80,51 @@ describe('PUT /tasks/:id', () => { expect(res.body.title).toBeDefined(); }); +}); + +const request = require('supertest'); +const app = require('../app'); + +describe('PATCH /tasks/:id/complete', () => { + + let taskId; + + beforeEach(async () => { + const res = await request(app).post('/tasks').send({ + title: 'Incomplete Task', + description: 'Needs completion', + status: 'todo', + priority: 'medium' + }); + + taskId = res.body.id; + }); + + it('should mark task as complete', async () => { + const res = await request(app) + .patch(`/tasks/${taskId}/complete`); + + expect(res.statusCode).toBe(200); + + expect(res.body.status).toBe('done'); + expect(res.body.completedAt).not.toBeNull(); + }); + + it('should not break if task is already completed', async () => { + await request(app).patch(`/tasks/${taskId}/complete`); + const res = await request(app).patch(`/tasks/${taskId}/complete`); + + expect(res.statusCode).toBe(200); + expect(res.body.status).toBe('done'); + }); + + it('should return 404 for non-existing task', async () => { + const fakeId = '123e4567-e89b-12d3-a456-426614174000'; + + const res = await request(app) + .patch(`/tasks/${fakeId}/complete`); + + expect(res.statusCode).toBe(404); + }); + }); \ No newline at end of file From 272d74a39d77c7d31864d1edb74f067198aa9caa Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 13:11:45 +0530 Subject: [PATCH 04/12] fix bug 1 --- task-api/src/services/taskService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/task-api/src/services/taskService.js b/task-api/src/services/taskService.js index f8e89189..f8280a8f 100644 --- a/task-api/src/services/taskService.js +++ b/task-api/src/services/taskService.js @@ -6,7 +6,7 @@ const getAll = () => [...tasks]; const findById = (id) => tasks.find((t) => t.id === id); -const getByStatus = (status) => tasks.filter((t) => t.status.includes(status)); +const getByStatus = (status) => tasks.filter((t) => t.status===status); const getPaginated = (page, limit) => { const offset = page * limit; From 35c685a540ef0098d47cf17d6902bbdc3b7d2316 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 13:48:35 +0530 Subject: [PATCH 05/12] bug 2 fix- changing page*limit to (page-1)*limit --- task-api/src/services/taskService.js | 2 +- task-api/tests/createTasks.test.js | 2 +- task-api/tests/deleteTasks.test.js | 2 +- task-api/tests/getTasks.test.js | 2 +- task-api/tests/updateTasks.test.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/task-api/src/services/taskService.js b/task-api/src/services/taskService.js index f8280a8f..6171f69b 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===status); const getPaginated = (page, limit) => { - const offset = page * limit; + const offset = (page-1) * limit; return tasks.slice(offset, offset + limit); }; diff --git a/task-api/tests/createTasks.test.js b/task-api/tests/createTasks.test.js index 448fa807..e5cb8792 100644 --- a/task-api/tests/createTasks.test.js +++ b/task-api/tests/createTasks.test.js @@ -1,5 +1,5 @@ const request = require('supertest'); -const app = require('../app'); +const app = require('../src/app'); describe('POST /tasks', () => { diff --git a/task-api/tests/deleteTasks.test.js b/task-api/tests/deleteTasks.test.js index 81befd88..47ee8ee4 100644 --- a/task-api/tests/deleteTasks.test.js +++ b/task-api/tests/deleteTasks.test.js @@ -1,5 +1,5 @@ const request = require('supertest'); -const app = require('../app'); +const app = require('../src/app'); describe('DELETE /tasks/:id', () => { diff --git a/task-api/tests/getTasks.test.js b/task-api/tests/getTasks.test.js index 4fca3a6f..ebe20371 100644 --- a/task-api/tests/getTasks.test.js +++ b/task-api/tests/getTasks.test.js @@ -1,5 +1,5 @@ const request = require('supertest'); -const app = require('../app'); +const app = require('../src/app'); describe('GET /tasks?status=todo', () => { diff --git a/task-api/tests/updateTasks.test.js b/task-api/tests/updateTasks.test.js index 28427783..15560725 100644 --- a/task-api/tests/updateTasks.test.js +++ b/task-api/tests/updateTasks.test.js @@ -1,5 +1,5 @@ const request = require('supertest'); -const app = require('../app'); +const app = require('../src/app'); describe('PUT /tasks/:id', () => { From 5784044c6b400dc9d191b2b2afa89a5ac713a278 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 21:19:19 +0530 Subject: [PATCH 06/12] #bug 3 fix hardcorded priority medium in complete task --- task-api/src/services/taskService.js | 1 - 1 file changed, 1 deletion(-) diff --git a/task-api/src/services/taskService.js b/task-api/src/services/taskService.js index 6171f69b..b863496e 100644 --- a/task-api/src/services/taskService.js +++ b/task-api/src/services/taskService.js @@ -66,7 +66,6 @@ const completeTask = (id) => { const updated = { ...task, - priority: 'medium', status: 'done', completedAt: new Date().toISOString(), }; From a3e2f59bb9ef02dcf10eabd6af88e33436086ada Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 21:23:12 +0530 Subject: [PATCH 07/12] fix BUG 4 Put on /takss/:id allows overwritten of protective field --- task-api/src/services/taskService.js | 10 +++++++++- task-api/src/utils/validators.js | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/task-api/src/services/taskService.js b/task-api/src/services/taskService.js index b863496e..a7bf3311 100644 --- a/task-api/src/services/taskService.js +++ b/task-api/src/services/taskService.js @@ -1,6 +1,7 @@ const { v4: uuidv4 } = require('uuid'); let tasks = []; +const UPDATABLE_FIELDS = ['title', 'description', 'status', 'priority', 'dueDate']; const getAll = () => [...tasks]; @@ -47,7 +48,14 @@ const update = (id, fields) => { const index = tasks.findIndex((t) => t.id === id); if (index === -1) return null; - const updated = { ...tasks[index], ...fields }; + const updates = UPDATABLE_FIELDS.reduce((acc, field) => { + if (fields[field] !== undefined) { + acc[field] = fields[field]; + } + return acc; + }, {}); + + const updated = { ...tasks[index], ...updates }; tasks[index] = updated; return updated; }; diff --git a/task-api/src/utils/validators.js b/task-api/src/utils/validators.js index 1e908ff5..7ce63c9a 100644 --- a/task-api/src/utils/validators.js +++ b/task-api/src/utils/validators.js @@ -1,5 +1,7 @@ const VALID_STATUSES = ['todo', 'in_progress', 'done']; const VALID_PRIORITIES = ['low', 'medium', 'high']; +const PROTECTED_UPDATE_FIELDS = ['id', 'createdAt', 'completedAt']; +const MUTABLE_UPDATE_FIELDS = ['title', 'description', 'status', 'priority', 'dueDate']; const validateCreateTask = (body) => { if (!body.title || typeof body.title !== 'string' || body.title.trim() === '') { @@ -18,6 +20,18 @@ const validateCreateTask = (body) => { }; const validateUpdateTask = (body) => { + const protectedField = PROTECTED_UPDATE_FIELDS.find((field) => body[field] !== undefined); + if (protectedField) { + return `${protectedField} cannot be updated`; + } + + const invalidField = Object.keys(body).find( + (field) => !MUTABLE_UPDATE_FIELDS.includes(field) && !PROTECTED_UPDATE_FIELDS.includes(field) + ); + if (invalidField) { + return `${invalidField} is not a valid task field`; + } + if (body.title !== undefined && (typeof body.title !== 'string' || body.title.trim() === '')) { return 'title must be a non-empty string'; } From 6a41f09161df2ab8849be39c12a84b1715f69728 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 21:25:34 +0530 Subject: [PATCH 08/12] fix BUG 5 Null/falsy status and priority bypass validation and corrupt tasks --- task-api/src/utils/validators.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/task-api/src/utils/validators.js b/task-api/src/utils/validators.js index 7ce63c9a..ce5a0d84 100644 --- a/task-api/src/utils/validators.js +++ b/task-api/src/utils/validators.js @@ -7,10 +7,10 @@ const validateCreateTask = (body) => { if (!body.title || typeof body.title !== 'string' || body.title.trim() === '') { return 'title is required and must be a non-empty string'; } - if (body.status && !VALID_STATUSES.includes(body.status)) { + if (body.status !== undefined && !VALID_STATUSES.includes(body.status)) { return `status must be one of: ${VALID_STATUSES.join(', ')}`; } - if (body.priority && !VALID_PRIORITIES.includes(body.priority)) { + if (body.priority !== undefined && !VALID_PRIORITIES.includes(body.priority)) { return `priority must be one of: ${VALID_PRIORITIES.join(', ')}`; } if (body.dueDate && isNaN(Date.parse(body.dueDate))) { @@ -35,10 +35,10 @@ const validateUpdateTask = (body) => { if (body.title !== undefined && (typeof body.title !== 'string' || body.title.trim() === '')) { return 'title must be a non-empty string'; } - if (body.status && !VALID_STATUSES.includes(body.status)) { + if (body.status !== undefined && !VALID_STATUSES.includes(body.status)) { return `status must be one of: ${VALID_STATUSES.join(', ')}`; } - if (body.priority && !VALID_PRIORITIES.includes(body.priority)) { + if (body.priority !== undefined && !VALID_PRIORITIES.includes(body.priority)) { return `priority must be one of: ${VALID_PRIORITIES.join(', ')}`; } if (body.dueDate && isNaN(Date.parse(body.dueDate))) { From 3195c46e7a1586a0d27a6a35be838f358542eafd Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 21:32:00 +0530 Subject: [PATCH 09/12] implement feature to fetch a perticular id and getpaginated has no bound checking --- task-api/src/routes/tasks.js | 13 +++++++++++-- task-api/src/services/taskService.js | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/task-api/src/routes/tasks.js b/task-api/src/routes/tasks.js index e8c370fe..1905283e 100644 --- a/task-api/src/routes/tasks.js +++ b/task-api/src/routes/tasks.js @@ -17,8 +17,8 @@ router.get('/', (req, res) => { } if (page !== undefined || limit !== undefined) { - const pageNum = parseInt(page) || 1; - const limitNum = parseInt(limit) || 10; + const pageNum = page === undefined ? 1 : parseInt(page, 10); + const limitNum = limit === undefined ? 10 : parseInt(limit, 10); const tasks = taskService.getPaginated(pageNum, limitNum); return res.json(tasks); } @@ -27,6 +27,15 @@ router.get('/', (req, res) => { res.json(tasks); }); +router.get('/:id', (req, res) => { + const task = taskService.findById(req.params.id); + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + + res.json(task); +}); + router.post('/', (req, res) => { const error = validateCreateTask(req.body); if (error) { diff --git a/task-api/src/services/taskService.js b/task-api/src/services/taskService.js index a7bf3311..1bbedb62 100644 --- a/task-api/src/services/taskService.js +++ b/task-api/src/services/taskService.js @@ -10,6 +10,10 @@ const findById = (id) => tasks.find((t) => t.id === id); const getByStatus = (status) => tasks.filter((t) => t.status===status); const getPaginated = (page, limit) => { + if (!Number.isInteger(page) || !Number.isInteger(limit) || page < 1 || limit < 1) { + return []; + } + const offset = (page-1) * limit; return tasks.slice(offset, offset + limit); }; From 18e91604863195ef4838991a7deb61729b9a1ff3 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 21:38:00 +0530 Subject: [PATCH 10/12] =?UTF-8?q?dueDate=20in=20the=20past=20is=20accepted?= =?UTF-8?q?=20without=20warning=20=E2=80=94=20no=20validation=20that=20the?= =?UTF-8?q?=20date=20is=20in=20the=20future=20implement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- task-api/src/utils/validators.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/task-api/src/utils/validators.js b/task-api/src/utils/validators.js index ce5a0d84..5653e7d8 100644 --- a/task-api/src/utils/validators.js +++ b/task-api/src/utils/validators.js @@ -3,6 +3,15 @@ const VALID_PRIORITIES = ['low', 'medium', 'high']; const PROTECTED_UPDATE_FIELDS = ['id', 'createdAt', 'completedAt']; const MUTABLE_UPDATE_FIELDS = ['title', 'description', 'status', 'priority', 'dueDate']; +const isValidFutureDueDate = (dueDate) => { + const parsedDate = Date.parse(dueDate); + if (isNaN(parsedDate)) { + return false; + } + + return parsedDate > Date.now(); +}; + const validateCreateTask = (body) => { if (!body.title || typeof body.title !== 'string' || body.title.trim() === '') { return 'title is required and must be a non-empty string'; @@ -13,8 +22,10 @@ const validateCreateTask = (body) => { if (body.priority !== undefined && !VALID_PRIORITIES.includes(body.priority)) { return `priority must be one of: ${VALID_PRIORITIES.join(', ')}`; } - if (body.dueDate && isNaN(Date.parse(body.dueDate))) { - return 'dueDate must be a valid ISO date string'; + if (body.dueDate !== undefined) { + if (!isValidFutureDueDate(body.dueDate)) { + return 'dueDate must be a valid ISO date string in the future'; + } } return null; }; @@ -41,8 +52,10 @@ const validateUpdateTask = (body) => { if (body.priority !== undefined && !VALID_PRIORITIES.includes(body.priority)) { return `priority must be one of: ${VALID_PRIORITIES.join(', ')}`; } - if (body.dueDate && isNaN(Date.parse(body.dueDate))) { - return 'dueDate must be a valid ISO date string'; + if (body.dueDate !== undefined) { + if (!isValidFutureDueDate(body.dueDate)) { + return 'dueDate must be a valid ISO date string in the future'; + } } return null; }; From 9c6fda555f7c6f81c5bcbfee0c617364866d106c Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 21:50:25 +0530 Subject: [PATCH 11/12] fix broken tests --- task-api/src/routes/tasks.js | 18 ++++++++++++++++-- task-api/tests/createTasks.test.js | 10 +++++++--- task-api/tests/deleteTasks.test.js | 7 ++++++- task-api/tests/getTasks.test.js | 18 ++++++++++++++---- task-api/tests/updateTasks.test.js | 10 ++++++---- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/task-api/src/routes/tasks.js b/task-api/src/routes/tasks.js index 1905283e..c6ff8e9a 100644 --- a/task-api/src/routes/tasks.js +++ b/task-api/src/routes/tasks.js @@ -2,6 +2,9 @@ const express = require('express'); const router = express.Router(); const taskService = require('../services/taskService'); const { validateCreateTask, validateUpdateTask } = require('../utils/validators'); +const VALID_STATUSES = ['todo', 'in_progress', 'done']; + +const isPositiveIntegerString = (value) => /^\d+$/.test(String(value)) && Number(value) > 0; router.get('/stats', (req, res) => { const stats = taskService.getStats(); @@ -12,13 +15,24 @@ router.get('/', (req, res) => { const { status, page, limit } = req.query; if (status) { + if (!VALID_STATUSES.includes(status)) { + return res.status(400).json({ error: `status must be one of: ${VALID_STATUSES.join(', ')}` }); + } const tasks = taskService.getByStatus(status); return res.json(tasks); } if (page !== undefined || limit !== undefined) { - const pageNum = page === undefined ? 1 : parseInt(page, 10); - const limitNum = limit === undefined ? 10 : parseInt(limit, 10); + if (page !== undefined && !isPositiveIntegerString(page)) { + return res.status(400).json({ error: 'page must be a positive integer' }); + } + + if (limit !== undefined && !isPositiveIntegerString(limit)) { + return res.status(400).json({ error: 'limit must be a positive integer' }); + } + + const pageNum = page === undefined ? 1 : Number(page); + const limitNum = limit === undefined ? 10 : Number(limit); const tasks = taskService.getPaginated(pageNum, limitNum); return res.json(tasks); } diff --git a/task-api/tests/createTasks.test.js b/task-api/tests/createTasks.test.js index e5cb8792..9921c8af 100644 --- a/task-api/tests/createTasks.test.js +++ b/task-api/tests/createTasks.test.js @@ -1,14 +1,18 @@ const request = require('supertest'); const app = require('../src/app'); +const taskService = require('../src/services/taskService'); -describe('POST /tasks', () => { +beforeEach(() => { + taskService._reset(); +}); +describe('POST /tasks', () => { const validTask = { title: 'Complete assignment', description: 'Write API tests', status: 'todo', priority: 'high', - dueDate: new Date().toISOString() + dueDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() }; it('should create a new task successfully', async () => { @@ -85,4 +89,4 @@ describe('POST /tasks', () => { expect([200, 201, 400]).toContain(res.statusCode); }); -}); \ No newline at end of file +}); diff --git a/task-api/tests/deleteTasks.test.js b/task-api/tests/deleteTasks.test.js index 47ee8ee4..5d630466 100644 --- a/task-api/tests/deleteTasks.test.js +++ b/task-api/tests/deleteTasks.test.js @@ -1,5 +1,10 @@ const request = require('supertest'); const app = require('../src/app'); +const taskService = require('../src/services/taskService'); + +beforeEach(() => { + taskService._reset(); +}); describe('DELETE /tasks/:id', () => { @@ -38,4 +43,4 @@ describe('DELETE /tasks/:id', () => { expect(res.statusCode).toBe(404); }); -}); \ No newline at end of file +}); diff --git a/task-api/tests/getTasks.test.js b/task-api/tests/getTasks.test.js index ebe20371..99948317 100644 --- a/task-api/tests/getTasks.test.js +++ b/task-api/tests/getTasks.test.js @@ -1,5 +1,10 @@ const request = require('supertest'); const app = require('../src/app'); +const taskService = require('../src/services/taskService'); + +beforeEach(() => { + taskService._reset(); +}); describe('GET /tasks?status=todo', () => { @@ -132,16 +137,19 @@ describe('GET /tasks/stats', () => { description: 'Task 3', status: 'done', priority: 'high', - dueDate: pastDate + dueDate: futureDate }); - await request(app).post('/tasks').send({ + const overdueTaskRes = await request(app).post('/tasks').send({ title: 'Overdue Task', description: 'Task 4', status: 'todo', priority: 'high', - dueDate: pastDate + dueDate: futureDate }); + + const overdueTask = taskService.findById(overdueTaskRes.body.id); + overdueTask.dueDate = pastDate; }); it('should return correct task statistics', async () => { @@ -161,6 +169,8 @@ describe('GET /tasks/stats', () => { }); it('should return zero counts when no tasks exist', async () => { + taskService._reset(); + const res = await request(app).get('/tasks/stats'); expect(res.statusCode).toBe(200); @@ -173,4 +183,4 @@ describe('GET /tasks/stats', () => { }); }); -}); \ No newline at end of file +}); diff --git a/task-api/tests/updateTasks.test.js b/task-api/tests/updateTasks.test.js index 15560725..ccf24db4 100644 --- a/task-api/tests/updateTasks.test.js +++ b/task-api/tests/updateTasks.test.js @@ -1,5 +1,10 @@ const request = require('supertest'); const app = require('../src/app'); +const taskService = require('../src/services/taskService'); + +beforeEach(() => { + taskService._reset(); +}); describe('PUT /tasks/:id', () => { @@ -82,9 +87,6 @@ describe('PUT /tasks/:id', () => { }); -const request = require('supertest'); -const app = require('../app'); - describe('PATCH /tasks/:id/complete', () => { let taskId; @@ -127,4 +129,4 @@ describe('PATCH /tasks/:id/complete', () => { expect(res.statusCode).toBe(404); }); -}); \ No newline at end of file +}); From b780afd17aa17dfed0659d6df5dc5fa18ef0ab87 Mon Sep 17 00:00:00 2001 From: Sudhanshu Date: Sat, 4 Apr 2026 21:56:07 +0530 Subject: [PATCH 12/12] implement the patch feature and created a submission.md file --- task-api/src/routes/tasks.js | 16 ++++++- task-api/src/services/taskService.js | 15 ++++++ task-api/src/utils/validators.js | 20 +++++++- task-api/tests/assignTasks.test.js | 72 ++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 task-api/tests/assignTasks.test.js diff --git a/task-api/src/routes/tasks.js b/task-api/src/routes/tasks.js index c6ff8e9a..1ad3d42b 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'); const VALID_STATUSES = ['todo', 'in_progress', 'done']; const isPositiveIntegerString = (value) => /^\d+$/.test(String(value)) && Number(value) > 0; @@ -92,4 +92,18 @@ 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.assignTask(req.params.id, req.body.assignee); + if (!task) { + return res.status(404).json({ error: 'Task not found' }); + } + + res.json(task); +}); + module.exports = router; diff --git a/task-api/src/services/taskService.js b/task-api/src/services/taskService.js index 1bbedb62..5d4c752d 100644 --- a/task-api/src/services/taskService.js +++ b/task-api/src/services/taskService.js @@ -41,6 +41,7 @@ const create = ({ title, description = '', status = 'todo', priority = 'medium', status, priority, dueDate, + assignee: null, completedAt: null, createdAt: new Date().toISOString(), }; @@ -87,6 +88,19 @@ const completeTask = (id) => { return updated; }; +const assignTask = (id, assignee) => { + const index = tasks.findIndex((t) => t.id === id); + if (index === -1) return null; + + const updated = { + ...tasks[index], + assignee: assignee.trim(), + }; + + tasks[index] = updated; + return updated; +}; + const _reset = () => { tasks = []; }; @@ -101,5 +115,6 @@ module.exports = { update, remove, completeTask, + assignTask, _reset, }; diff --git a/task-api/src/utils/validators.js b/task-api/src/utils/validators.js index 5653e7d8..e6b37ee5 100644 --- a/task-api/src/utils/validators.js +++ b/task-api/src/utils/validators.js @@ -2,6 +2,7 @@ const VALID_STATUSES = ['todo', 'in_progress', 'done']; const VALID_PRIORITIES = ['low', 'medium', 'high']; const PROTECTED_UPDATE_FIELDS = ['id', 'createdAt', 'completedAt']; const MUTABLE_UPDATE_FIELDS = ['title', 'description', 'status', 'priority', 'dueDate']; +const VALID_ASSIGN_FIELDS = ['assignee']; const isValidFutureDueDate = (dueDate) => { const parsedDate = Date.parse(dueDate); @@ -60,4 +61,21 @@ const validateUpdateTask = (body) => { return null; }; -module.exports = { validateCreateTask, validateUpdateTask }; +const validateAssignTask = (body) => { + const invalidField = Object.keys(body).find((field) => !VALID_ASSIGN_FIELDS.includes(field)); + if (invalidField) { + return `${invalidField} is not a valid task field`; + } + + 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, validateAssignTask }; diff --git a/task-api/tests/assignTasks.test.js b/task-api/tests/assignTasks.test.js new file mode 100644 index 00000000..87429dea --- /dev/null +++ b/task-api/tests/assignTasks.test.js @@ -0,0 +1,72 @@ +const request = require('supertest'); +const app = require('../src/app'); +const taskService = require('../src/services/taskService'); + +beforeEach(() => { + taskService._reset(); +}); + +describe('PATCH /tasks/:id/assign', () => { + let taskId; + + beforeEach(async () => { + const res = await request(app).post('/tasks').send({ + title: 'Unassigned Task', + description: 'Needs a person', + status: 'todo', + priority: 'medium' + }); + + taskId = res.body.id; + }); + + it('should assign a task to a user', async () => { + const res = await request(app) + .patch(`/tasks/${taskId}/assign`) + .send({ assignee: 'Ada Lovelace' }); + + expect(res.statusCode).toBe(200); + expect(res.body.id).toBe(taskId); + expect(res.body.assignee).toBe('Ada Lovelace'); + }); + + it('should trim whitespace from assignee names', async () => { + const res = await request(app) + .patch(`/tasks/${taskId}/assign`) + .send({ assignee: ' Grace Hopper ' }); + + expect(res.statusCode).toBe(200); + expect(res.body.assignee).toBe('Grace Hopper'); + }); + + it('should allow reassignment', async () => { + await request(app) + .patch(`/tasks/${taskId}/assign`) + .send({ assignee: 'First Person' }); + + const res = await request(app) + .patch(`/tasks/${taskId}/assign`) + .send({ assignee: 'Second Person' }); + + expect(res.statusCode).toBe(200); + expect(res.body.assignee).toBe('Second Person'); + }); + + it('should return 400 for empty assignee', async () => { + const res = await request(app) + .patch(`/tasks/${taskId}/assign`) + .send({ assignee: ' ' }); + + expect(res.statusCode).toBe(400); + }); + + it('should return 404 for non-existing task', async () => { + const fakeId = '123e4567-e89b-12d3-a456-426614174000'; + + const res = await request(app) + .patch(`/tasks/${fakeId}/assign`) + .send({ assignee: 'Ada Lovelace' }); + + expect(res.statusCode).toBe(404); + }); +});