From 8c505f7f99872df85b473462450990c98ea2f6b0 Mon Sep 17 00:00:00 2001 From: AJ Enns Date: Tue, 23 Jun 2026 23:12:03 -0500 Subject: [PATCH] Add input validation to POST /tasks Reject missing, non-string, or empty/whitespace-only titles with a 400 and a clear JSON error before any task is created. Trim the title before storing on the happy path. Add tests covering the happy path, whitespace trimming, and the invalid cases (missing, empty/whitespace, and non-string types). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sample-app/src/app.test.ts | 52 ++++++++++++++++++++++++++++++++++ sample-app/src/tasks/routes.ts | 9 ++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/sample-app/src/app.test.ts b/sample-app/src/app.test.ts index c2d8dfe..f53e852 100644 --- a/sample-app/src/app.test.ts +++ b/sample-app/src/app.test.ts @@ -23,6 +23,58 @@ describe('Task API', () => { expect(list.body).toHaveLength(1); }); + it('trims surrounding whitespace from the title', async () => { + const app = createApp(); + const created = await request(app).post('/tasks').send({ title: ' Padded title ' }); + + expect(created.status).toBe(201); + expect(created.body.title).toBe('Padded title'); + }); + + it('rejects a missing title with 400 and creates no task', async () => { + const app = createApp(); + + const res = await request(app).post('/tasks').send({}); + expect(res.status).toBe(400); + expect(res.body).toEqual({ + error: 'title is required and must be a non-empty string', + }); + + const list = await request(app).get('/tasks'); + expect(list.body).toHaveLength(0); + }); + + it('rejects an empty or whitespace-only title with 400 and creates no task', async () => { + const app = createApp(); + + const empty = await request(app).post('/tasks').send({ title: '' }); + expect(empty.status).toBe(400); + expect(empty.body.error).toContain('title'); + + const whitespace = await request(app).post('/tasks').send({ title: ' ' }); + expect(whitespace.status).toBe(400); + + const list = await request(app).get('/tasks'); + expect(list.body).toHaveLength(0); + }); + + it.each([ + ['a number', 123], + ['null', null], + ['a boolean', true], + ['an array', []], + ['an object', {}], + ])('rejects a non-string title (%s) with 400 and creates no task', async (_label, title) => { + const app = createApp(); + + const res = await request(app).post('/tasks').send({ title }); + expect(res.status).toBe(400); + expect(res.body.error).toContain('title'); + + const list = await request(app).get('/tasks'); + expect(list.body).toHaveLength(0); + }); + it('marks a task completed via PATCH', async () => { const app = createApp(); const created = await request(app).post('/tasks').send({ title: 'Finish slides' }); diff --git a/sample-app/src/tasks/routes.ts b/sample-app/src/tasks/routes.ts index e3eabca..62eca44 100644 --- a/sample-app/src/tasks/routes.ts +++ b/sample-app/src/tasks/routes.ts @@ -10,10 +10,13 @@ export function createTaskRouter(store: TaskStore): Router { }); router.post('/', (req, res) => { - // Exercise 1: validate that req.body.title is a non-empty string and - // return 400 with a helpful message when it is not. const { title } = req.body ?? {}; - const task = store.create(title); + if (typeof title !== 'string' || title.trim().length === 0) { + return res + .status(400) + .json({ error: 'title is required and must be a non-empty string' }); + } + const task = store.create(title.trim()); res.status(201).json(task); });