mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
130 lines
5.7 KiB
TypeScript
130 lines
5.7 KiB
TypeScript
import { TodoController } from '../../../src/nest/todo/todo.controller';
|
|
import type { TodoService } from '../../../src/nest/todo/todo.service';
|
|
import type { User } from '../../../src/types';
|
|
import { HttpException } from '@nestjs/common';
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
|
|
const user = { id: 1, role: 'user', email: 'u@example.test' } as User;
|
|
const trip = { id: 5, user_id: 1 };
|
|
|
|
function makeService(overrides: Partial<TodoService> = {}): TodoService {
|
|
return {
|
|
verifyTripAccess: vi.fn().mockReturnValue(trip),
|
|
canEdit: vi.fn().mockReturnValue(true),
|
|
broadcast: vi.fn(),
|
|
...overrides,
|
|
} as unknown as TodoService;
|
|
}
|
|
|
|
async function thrown(fn: () => unknown): Promise<{ status: number; body: unknown }> {
|
|
try {
|
|
await fn();
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(HttpException);
|
|
const e = err as HttpException;
|
|
return { status: e.getStatus(), body: e.getResponse() };
|
|
}
|
|
throw new Error('expected the handler to throw');
|
|
}
|
|
|
|
// The 404 "Trip not found" and 403 "No permission" cases moved to
|
|
// trip-access.guard.test.ts with the check itself.
|
|
describe('TodoController (parity with the legacy /api/trips/:tripId/todo route)', () => {
|
|
it('GET / returns items', async () => {
|
|
const svc = makeService({ listItems: vi.fn().mockResolvedValue([{ id: 1 }]) } as Partial<TodoService>);
|
|
expect(await new TodoController(svc).list(user, '5')).toEqual({ items: [{ id: 1 }] });
|
|
});
|
|
|
|
describe('POST /', () => {
|
|
// The missing-name 400 moved from a bespoke controller check into the
|
|
// ZodValidationPipe (todoCreateItemRequestSchema) — direct method calls
|
|
// bypass parameter pipes, so that path is covered by the e2e suite and
|
|
// the schema spec in @trek/shared.
|
|
|
|
it('creates and broadcasts', async () => {
|
|
const createItem = vi.fn().mockResolvedValue({ id: 9, name: 'Pack' });
|
|
const broadcast = vi.fn();
|
|
const svc = makeService({ createItem, broadcast } as Partial<TodoService>);
|
|
expect(await new TodoController(svc).create(user, '5', { name: 'Pack', priority: 2 }, 'sock')).toEqual({
|
|
item: { id: 9, name: 'Pack' },
|
|
});
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'todo:created', { item: { id: 9, name: 'Pack' } }, 'sock');
|
|
});
|
|
});
|
|
|
|
describe('PUT /:id', () => {
|
|
it('404 when item missing', async () => {
|
|
const svc = makeService({ updateItem: vi.fn().mockResolvedValue(null) } as Partial<TodoService>);
|
|
expect(await thrown(() => new TodoController(svc).update(user, '5', '9', { name: 'X' }))).toEqual({
|
|
status: 404,
|
|
body: { error: 'Item not found' },
|
|
});
|
|
});
|
|
|
|
it('updates, forwards changed keys, normalizes checked to 0/1, broadcasts', async () => {
|
|
const updateItem = vi.fn().mockResolvedValue({ id: 9 });
|
|
const broadcast = vi.fn();
|
|
const svc = makeService({ updateItem, broadcast } as Partial<TodoService>);
|
|
await new TodoController(svc).update(user, '5', '9', { checked: true }, 'sock');
|
|
expect(updateItem).toHaveBeenCalledWith('5', 9, expect.objectContaining({ checked: 1 }), ['checked']);
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'todo:updated', { item: { id: 9 } }, 'sock');
|
|
});
|
|
|
|
it('normalizes checked false to 0', async () => {
|
|
const updateItem = vi.fn().mockResolvedValue({ id: 9 });
|
|
const svc = makeService({ updateItem, broadcast: vi.fn() } as Partial<TodoService>);
|
|
await new TodoController(svc).update(user, '5', '9', { checked: false });
|
|
expect(updateItem).toHaveBeenCalledWith('5', 9, expect.objectContaining({ checked: 0 }), ['checked']);
|
|
});
|
|
});
|
|
|
|
describe('DELETE /:id', () => {
|
|
it('404 when item missing', async () => {
|
|
const svc = makeService({ deleteItem: vi.fn().mockResolvedValue(false) } as Partial<TodoService>);
|
|
expect(await thrown(() => new TodoController(svc).remove(user, '5', '9'))).toEqual({
|
|
status: 404,
|
|
body: { error: 'Item not found' },
|
|
});
|
|
});
|
|
|
|
it('deletes and broadcasts', async () => {
|
|
const deleteItem = vi.fn().mockResolvedValue(true);
|
|
const broadcast = vi.fn();
|
|
const svc = makeService({ deleteItem, broadcast } as Partial<TodoService>);
|
|
expect(await new TodoController(svc).remove(user, '5', '9', 'sock')).toEqual({ success: true });
|
|
expect(broadcast).toHaveBeenCalledWith('5', 'todo:deleted', { itemId: 9 }, 'sock');
|
|
});
|
|
});
|
|
|
|
it('PUT /reorder succeeds with permission', async () => {
|
|
const reorderItems = vi.fn().mockResolvedValue(undefined);
|
|
const svc = makeService({ reorderItems } as Partial<TodoService>);
|
|
expect(await new TodoController(svc).reorder(user, '5', { orderedIds: [3, 1, 2] })).toEqual({ success: true });
|
|
expect(reorderItems).toHaveBeenCalledWith('5', [3, 1, 2]);
|
|
});
|
|
|
|
describe('category assignees', () => {
|
|
it('GET returns assignees', async () => {
|
|
const svc = makeService({
|
|
getCategoryAssignees: vi.fn().mockResolvedValue([{ user_id: 2 }]),
|
|
} as Partial<TodoService>);
|
|
expect(await new TodoController(svc).categoryAssignees(user, '5')).toEqual({ assignees: [{ user_id: 2 }] });
|
|
});
|
|
|
|
it('PUT updates, decodes the category and broadcasts', async () => {
|
|
const updateCategoryAssignees = vi.fn().mockResolvedValue([{ user_id: 2 }]);
|
|
const broadcast = vi.fn();
|
|
const svc = makeService({ updateCategoryAssignees, broadcast } as Partial<TodoService>);
|
|
await new TodoController(svc).updateCategoryAssignees(user, '5', 'To%20Buy', { user_ids: [2] }, 'sock');
|
|
expect(updateCategoryAssignees).toHaveBeenCalledWith('5', 'To Buy', [2]);
|
|
expect(broadcast).toHaveBeenCalledWith(
|
|
'5',
|
|
'todo:assignees',
|
|
{ category: 'To Buy', assignees: [{ user_id: 2 }] },
|
|
'sock',
|
|
);
|
|
});
|
|
});
|
|
});
|