Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion sample-app/src/app.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest';
import { describe, expect, it } from 'vitest';
import request from 'supertest';
import { createApp } from './app.js';

Expand All @@ -23,6 +23,36 @@ describe('Task API', () => {
expect(list.body).toHaveLength(1);
});

it('trims surrounding whitespace from task titles', async () => {
const app = createApp();

const created = await request(app).post('/tasks').send({ title: ' Trim me ' });
expect(created.status).toBe(201);
expect(created.body.title).toBe('Trim me');

const list = await request(app).get('/tasks');
expect(list.status).toBe(200);
expect(list.body).toHaveLength(1);
expect(list.body[0].title).toBe('Trim me');
});

it.each([
{ body: {}, expectedMessage: 'Title must be a non-empty string' },
{ body: { title: '' }, expectedMessage: 'Title must be a non-empty string' },
{ body: { title: ' ' }, expectedMessage: 'Title must be a non-empty string' },
{ body: { title: 42 }, expectedMessage: 'Title must be a non-empty string' },
])('rejects invalid task titles: $body', async ({ body, expectedMessage }) => {
const app = createApp();

const res = await request(app).post('/tasks').send(body);
const list = await request(app).get('/tasks');

expect(res.status).toBe(400);
expect(res.body).toEqual({ error: expectedMessage });
expect(list.status).toBe(200);
expect(list.body).toEqual([]);
});

it('marks a task completed via PATCH', async () => {
const app = createApp();
const created = await request(app).post('/tasks').send({ title: 'Finish slides' });
Expand Down
17 changes: 12 additions & 5 deletions sample-app/src/tasks/routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Router } from 'express';
import type { TaskStore } from './store.js';
import { TASK_TITLE_ERROR, normalizeTaskTitle } from './validation.js';

export function createTaskRouter(store: TaskStore): Router {
const router = Router();
Expand All @@ -10,11 +11,17 @@ 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);
res.status(201).json(task);
try {
const title = normalizeTaskTitle(req.body?.title);
const task = store.create(title);
return res.status(201).json(task);
} catch (error) {
if (error instanceof Error && error.message === TASK_TITLE_ERROR) {
return res.status(400).json({ error: error.message });
}

throw error;
}
});

router.get('/:id', (req, res) => {
Expand Down
14 changes: 14 additions & 0 deletions sample-app/src/tasks/store.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { TaskStore } from './store.js';
import { TASK_TITLE_ERROR } from './validation.js';

describe('TaskStore', () => {
it('creates a task with sensible defaults', () => {
Expand All @@ -12,6 +13,19 @@ describe('TaskStore', () => {
expect(task.createdAt).toBe(task.updatedAt);
});

it('trims surrounding whitespace from task titles', () => {
const store = new TaskStore();
const task = store.create(' Write the talk ');

expect(task.title).toBe('Write the talk');
});

it('rejects blank task titles', () => {
const store = new TaskStore();

expect(() => store.create(' ')).toThrow(TASK_TITLE_ERROR);
});

it('lists every created task', () => {
const store = new TaskStore();
store.create('a');
Expand Down
4 changes: 3 additions & 1 deletion sample-app/src/tasks/store.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { randomUUID } from 'node:crypto';
import type { Task } from './types.js';
import { normalizeTaskTitle } from './validation.js';

/**
* In-memory task store. Intentionally small so it is easy to reason about
Expand All @@ -18,10 +19,11 @@ export class TaskStore {
}

create(title: string): Task {
const normalizedTitle = normalizeTaskTitle(title);
const now = new Date().toISOString();
const task: Task = {
id: randomUUID(),
title,
title: normalizedTitle,
completed: false,
createdAt: now,
updatedAt: now,
Expand Down
9 changes: 9 additions & 0 deletions sample-app/src/tasks/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const TASK_TITLE_ERROR = 'Title must be a non-empty string';

export function normalizeTaskTitle(title: unknown): string {
if (typeof title !== 'string' || title.trim().length === 0) {
throw new Error(TASK_TITLE_ERROR);
}

return title.trim();
}
Loading