TASK-001: implement todo backend
This commit is contained in:
209
tests/todos.test.js
Normal file
209
tests/todos.test.js
Normal file
@@ -0,0 +1,209 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const todoStore = [];
|
||||
let nextId = 1;
|
||||
|
||||
const prismaMock = {
|
||||
todo: {
|
||||
findMany: async () => [...todoStore],
|
||||
findUnique: async ({ where: { id } }) => todoStore.find((todo) => todo.id === id) ?? null,
|
||||
create: async ({ data }) => {
|
||||
const todo = {
|
||||
id: nextId++,
|
||||
title: data.title,
|
||||
description: data.description ?? null,
|
||||
done: data.done ?? false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
todoStore.push(todo);
|
||||
return todo;
|
||||
},
|
||||
update: async ({ where: { id }, data }) => {
|
||||
const index = todoStore.findIndex((todo) => todo.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
const error = new Error('Record to update not found.');
|
||||
error.code = 'P2025';
|
||||
throw error;
|
||||
}
|
||||
|
||||
todoStore[index] = {
|
||||
...todoStore[index],
|
||||
...data,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return todoStore[index];
|
||||
},
|
||||
delete: async ({ where: { id } }) => {
|
||||
const index = todoStore.findIndex((todo) => todo.id === id);
|
||||
|
||||
if (index === -1) {
|
||||
const error = new Error('Record to delete not found.');
|
||||
error.code = 'P2025';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const [removed] = todoStore.splice(index, 1);
|
||||
return removed;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
require.cache[require.resolve('../src/lib/prisma')] = {
|
||||
exports: prismaMock,
|
||||
};
|
||||
|
||||
const app = require('../src/app');
|
||||
|
||||
let server;
|
||||
let baseUrl;
|
||||
|
||||
async function api(path, { method = 'GET', body } = {}) {
|
||||
const response = await fetch(`${baseUrl}${path}`, {
|
||||
method,
|
||||
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
const json = text ? JSON.parse(text) : null;
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
body: json,
|
||||
};
|
||||
}
|
||||
|
||||
test.before(async () => {
|
||||
server = app.listen(0);
|
||||
await new Promise((resolve) => server.once('listening', resolve));
|
||||
const { port } = server.address();
|
||||
baseUrl = `http://127.0.0.1:${port}`;
|
||||
});
|
||||
|
||||
test.after(async () => {
|
||||
if (server) {
|
||||
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(() => {
|
||||
todoStore.length = 0;
|
||||
nextId = 1;
|
||||
});
|
||||
|
||||
test('POST /todos creates todo', async () => {
|
||||
const response = await api('/todos', {
|
||||
method: 'POST',
|
||||
body: { title: '테스트', description: '설명' },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 201);
|
||||
assert.equal(response.body.title, '테스트');
|
||||
assert.equal(response.body.done, false);
|
||||
assert.equal(todoStore.length, 1);
|
||||
});
|
||||
|
||||
test('GET /todos returns todos', async () => {
|
||||
await prismaMock.todo.create({ data: { title: '할 일 1', description: null } });
|
||||
await prismaMock.todo.create({ data: { title: '할 일 2', description: '설명', done: true } });
|
||||
|
||||
const response = await api('/todos');
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.body.length, 2);
|
||||
assert.equal(response.body[0].id, 1);
|
||||
assert.equal(response.body[1].done, true);
|
||||
});
|
||||
|
||||
test('GET /todos/:id returns a todo', async () => {
|
||||
const created = await prismaMock.todo.create({ data: { title: '조회 대상', description: '상세' } });
|
||||
|
||||
const response = await api(`/todos/${created.id}`);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.body.id, created.id);
|
||||
assert.equal(response.body.title, '조회 대상');
|
||||
});
|
||||
|
||||
test('PUT /todos/:id updates todo fields', async () => {
|
||||
const created = await prismaMock.todo.create({ data: { title: '원본', description: null } });
|
||||
|
||||
const response = await api(`/todos/${created.id}`, {
|
||||
method: 'PUT',
|
||||
body: { title: '수정됨', description: '업데이트', done: true },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.body.title, '수정됨');
|
||||
assert.equal(response.body.done, true);
|
||||
});
|
||||
|
||||
test('PATCH /todos/:id/done updates done only', async () => {
|
||||
const created = await prismaMock.todo.create({ data: { title: '원본', description: null } });
|
||||
|
||||
const response = await api(`/todos/${created.id}/done`, {
|
||||
method: 'PATCH',
|
||||
body: { done: true },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(response.body.done, true);
|
||||
assert.equal(response.body.title, '원본');
|
||||
});
|
||||
|
||||
test('DELETE /todos/:id removes todo', async () => {
|
||||
const created = await prismaMock.todo.create({ data: { title: '삭제 대상', description: null } });
|
||||
|
||||
const response = await api(`/todos/${created.id}`, { method: 'DELETE' });
|
||||
|
||||
assert.equal(response.status, 204);
|
||||
assert.equal(todoStore.length, 0);
|
||||
});
|
||||
|
||||
test('returns 400 for invalid done payload', async () => {
|
||||
const created = await prismaMock.todo.create({ data: { title: '원본', description: null } });
|
||||
|
||||
const response = await api(`/todos/${created.id}/done`, {
|
||||
method: 'PATCH',
|
||||
body: { done: 'yes' },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(response.body.message, /boolean/);
|
||||
});
|
||||
|
||||
test('returns 400 for empty title on create', async () => {
|
||||
const response = await api('/todos', {
|
||||
method: 'POST',
|
||||
body: { title: ' ' },
|
||||
});
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(response.body.message, /title is required/i);
|
||||
});
|
||||
|
||||
test('returns 400 for invalid todo id', async () => {
|
||||
const response = await api('/todos/abc');
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
assert.match(response.body.message, /invalid todo id/i);
|
||||
});
|
||||
|
||||
test('returns 404 when todo is missing', async () => {
|
||||
const response = await api('/todos/999');
|
||||
|
||||
assert.equal(response.status, 404);
|
||||
assert.match(response.body.message, /todo not found/i);
|
||||
});
|
||||
|
||||
test('returns 404 for unknown route', async () => {
|
||||
const response = await api('/unknown');
|
||||
|
||||
assert.equal(response.status, 404);
|
||||
assert.match(response.body.message, /route not found/i);
|
||||
});
|
||||
Reference in New Issue
Block a user