From 19497317731b58f9c2a0ed236df09aebb4379bd6 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 13 Aug 2025 09:57:05 -0400 Subject: [PATCH] feat(tests): setup e2e-spec files for route testing. shifts, expenses, timesheets --- docs/swagger/swagger-spec.json | 41 +++++- .../expenses/dtos/create-expense.dto.ts | 15 ++- .../shifts/controllers/shifts.controller.ts | 2 +- src/modules/shifts/dtos/create-shift.dto.ts | 3 +- .../shifts/services/shifts-query.service.ts | 7 +- .../timesheets/dtos/create-timesheet.dto.ts | 5 +- test/expenses.e2e-spec.ts | 111 ++++++++++++++-- test/factories/customer.factory.ts | 0 test/factories/expense.factory.ts | 37 ++++++ test/factories/shift.factory.ts | 43 +++++++ test/factories/timesheet.factory.ts | 39 ++++++ test/shifts.e2e-spec.ts | 118 ++++++++++++++++-- test/timesheets.e2e-spec.ts | 105 ++++++++++++++-- 13 files changed, 478 insertions(+), 48 deletions(-) delete mode 100644 test/factories/customer.factory.ts diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 8bce1b6..14aa1c7 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -835,11 +835,24 @@ ] }, "get": { - "operationId": "ShiftsController_getSummary", + "operationId": "ShiftsController_findAll", "parameters": [], "responses": { - "200": { - "description": "" + "201": { + "description": "List of shifts found", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CreateShiftDto" + } + } + } + } + }, + "400": { + "description": "List of shifts not found" } }, "security": [ @@ -847,6 +860,7 @@ "access-token": [] } ], + "summary": "Find all shifts", "tags": [ "Shifts" ] @@ -1003,6 +1017,25 @@ ] } }, + "/shifts/summary": { + "get": { + "operationId": "ShiftsController_getSummary", + "parameters": [], + "responses": { + "200": { + "description": "" + } + }, + "security": [ + { + "access-token": [] + } + ], + "tags": [ + "Shifts" + ] + } + }, "/shifts/export.csv": { "get": { "operationId": "ShiftsController_exportCsv", @@ -2361,7 +2394,6 @@ "description": "ID number of an bank code (link with bank-codes)" }, "date": { - "format": "date-time", "type": "string", "example": "3018-10-20T00:00:00.000Z", "description": "Date where the expense was made" @@ -2417,7 +2449,6 @@ "description": "ID number of an bank code (link with bank-codes)" }, "date": { - "format": "date-time", "type": "string", "example": "3018-10-20T00:00:00.000Z", "description": "Date where the expense was made" diff --git a/src/modules/expenses/dtos/create-expense.dto.ts b/src/modules/expenses/dtos/create-expense.dto.ts index 4dcf1e4..2958e88 100644 --- a/src/modules/expenses/dtos/create-expense.dto.ts +++ b/src/modules/expenses/dtos/create-expense.dto.ts @@ -1,14 +1,14 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsBoolean, IsDate, IsDateString, IsInt, IsOptional, IsString } from "class-validator"; +import { Allow, IsBoolean, IsDate, IsDateString, IsInt, IsNumber, IsOptional, IsString } from "class-validator"; export class CreateExpenseDto { @ApiProperty({ example: 1, description: 'Unique ID of the expense (auto-generated)', }) - id: number; - + @Allow() + id?: number; @ApiProperty({ example: 101, @@ -31,17 +31,15 @@ export class CreateExpenseDto { description: 'Date where the expense was made', }) @IsDateString() - @Type(() => Date) - @IsDate() - date: Date; + date: string; @ApiProperty({ example: 17.82, description: 'amount in $ for a refund', }) @Type(() => Number) - @IsInt() - amount: number + @IsNumber() + amount: number; @ApiProperty({ example:'Spent for mileage between A and B', @@ -63,5 +61,6 @@ export class CreateExpenseDto { description:'Supervisro`s justification for the spending of an employee' }) @IsString() + @IsOptional() supervisor_comment?: string; } diff --git a/src/modules/shifts/controllers/shifts.controller.ts b/src/modules/shifts/controllers/shifts.controller.ts index 033616d..966dc9b 100644 --- a/src/modules/shifts/controllers/shifts.controller.ts +++ b/src/modules/shifts/controllers/shifts.controller.ts @@ -73,7 +73,7 @@ export class ShiftsController { return this.shiftsApprovalService.updateApproval(id, isApproved); } - @Get() + @Get('summary') async getSummary( @Query() query: GetShiftsOverviewDto): Promise { return this.shiftsValidationService.getSummary(query.period_id); } diff --git a/src/modules/shifts/dtos/create-shift.dto.ts b/src/modules/shifts/dtos/create-shift.dto.ts index 8c90160..b64dded 100644 --- a/src/modules/shifts/dtos/create-shift.dto.ts +++ b/src/modules/shifts/dtos/create-shift.dto.ts @@ -1,12 +1,13 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsDate, IsDateString, IsInt, IsString } from "class-validator"; +import { Allow, IsDate, IsDateString, IsInt, IsString } from "class-validator"; export class CreateShiftDto { @ApiProperty({ example: 1, description: 'Unique ID of the shift (auto-generated)', }) + @Allow() id: number; @ApiProperty({ diff --git a/src/modules/shifts/services/shifts-query.service.ts b/src/modules/shifts/services/shifts-query.service.ts index f2f8186..6a73100 100644 --- a/src/modules/shifts/services/shifts-query.service.ts +++ b/src/modules/shifts/services/shifts-query.service.ts @@ -29,11 +29,11 @@ export class ShiftsQueryService { ) {} async create(dto: CreateShiftDto): Promise { - const { timesheet_id, bank_code_id, date, start_time, end_time } = dto; + const { timesheet_id, bank_code_id, date, start_time, end_time, description } = dto; //shift creation const shift = await this.prisma.shifts.create({ - data: { timesheet_id, bank_code_id, date, start_time, end_time }, + data: { timesheet_id, bank_code_id, date, start_time, end_time, description }, include: { timesheet: { include: { employee: { include: { user: true } } } }, bank_code: true, }, @@ -92,7 +92,7 @@ export class ShiftsQueryService { async update(id: number, dto: UpdateShiftsDto): Promise { await this.findOne(id); - const { timesheet_id, bank_code_id, date,start_time,end_time} = dto; + const { timesheet_id, bank_code_id, date,start_time,end_time, description} = dto; return this.prisma.shifts.update({ where: { id }, data: { @@ -101,6 +101,7 @@ export class ShiftsQueryService { ...(date !== undefined && { date }), ...(start_time !== undefined && { start_time }), ...(end_time !== undefined && { end_time }), + ...(description !== undefined && { description }), }, include: { timesheet: { include: { employee: { include: { user: true } } } }, bank_code: true, diff --git a/src/modules/timesheets/dtos/create-timesheet.dto.ts b/src/modules/timesheets/dtos/create-timesheet.dto.ts index b8a24a2..6a1ace2 100644 --- a/src/modules/timesheets/dtos/create-timesheet.dto.ts +++ b/src/modules/timesheets/dtos/create-timesheet.dto.ts @@ -1,13 +1,14 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsBoolean, IsInt, IsOptional } from "class-validator"; +import { Allow, IsBoolean, IsInt, IsOptional } from "class-validator"; export class CreateTimesheetDto { @ApiProperty({ example: 1, description: 'timesheet`s unique ID (auto-generated)', }) - id: number; + @Allow() + id?: number; @ApiProperty({ example: 426433, diff --git a/test/expenses.e2e-spec.ts b/test/expenses.e2e-spec.ts index a7a5113..d4a5cb6 100644 --- a/test/expenses.e2e-spec.ts +++ b/test/expenses.e2e-spec.ts @@ -1,25 +1,120 @@ import * as request from 'supertest'; import { INestApplication } from '@nestjs/common'; import { createApp } from './utils/testing-app'; -// import { resetDb } from './utils/reset-db'; import { PrismaService } from 'src/prisma/prisma.service'; +import { makeExpense, makeInvalidExpense } from './factories/expense.factory'; -describe('Expenses (e2e)', () => { +const BASE = '/Expenses'; + +describe('Expenses (e2e) — autonome', () => { let app: INestApplication; - const BASE = '/expenses'; + let prisma: PrismaService; + let createdId: number | null = null; - beforeAll(async () => { app = await createApp(); }); - beforeEach(async () => { - // await resetDb(app); + let tsId: number; + let bcExpenseId: number; // categorie='EXPENSE' & type != 'MILEAGE' + + beforeAll(async () => { + app = await createApp(); + prisma = app.get(PrismaService); + + const ts = await prisma.timesheets.findFirst({ select: { id: true } }); + if (!ts) throw new Error('No timesheet found — seed timesheets first.'); + tsId = ts.id; + + const bc = await prisma.bankCodes.findFirst({ + where: { + categorie: 'EXPENSE', + NOT: { type: 'MILEAGE' }, // évite la branche mileageService + }, + select: { id: true }, + }); + if (!bc) throw new Error("No non-MILEAGE EXPENSE bank code found — seed bank codes first."); + bcExpenseId = bc.id; }); + afterAll(async () => { - const prisma = app.get(PrismaService); + if (createdId) { + try { await prisma.expenses.delete({ where: { id: createdId } }); } catch {} + } await app.close(); await prisma.$disconnect(); }); - it(`GET ${BASE} → 200`, async () => { + it(`GET ${BASE} → 200 (array)`, async () => { const res = await request(app.getHttpServer()).get(BASE); expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => { + const payload = makeExpense(tsId, bcExpenseId); + const createRes = await request(app.getHttpServer()).post(BASE).send(payload); + + if (createRes.status !== 201) { + console.log('Create error:', createRes.body || createRes.text); + } + expect(createRes.status).toBe(201); + expect(createRes.body).toEqual( + expect.objectContaining({ + id: expect.any(Number), + timesheet_id: tsId, + bank_code_id: bcExpenseId, + }) + ); + createdId = createRes.body.id; + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); + expect(getRes.status).toBe(200); + expect(getRes.body).toEqual(expect.objectContaining({ id: createdId })); + }); + + it(`PATCH ${BASE}/:id → 200 (amount/description mis à jour)`, async () => { + if (!createdId) { + const created = await request(app.getHttpServer()) + .post(BASE) + .send(makeExpense(tsId, bcExpenseId)); + expect(created.status).toBe(201); + createdId = created.body.id; + } + + const updated = { amount: 123, description: 'Updated expense' }; // amount INT + const patchRes = await request(app.getHttpServer()) + .patch(`${BASE}/${createdId}`) + .send(updated); + expect([200, 204]).toContain(patchRes.status); + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); + expect(getRes.status).toBe(200); + if (getRes.body?.amount !== undefined) { + expect(Number(getRes.body.amount)).toBe(updated.amount); + } + if (getRes.body?.description !== undefined) expect(getRes.body.description).toBe(updated.description); + }); + + it(`GET ${BASE}/999999 (not found) → 404/400`, async () => { + const res = await request(app.getHttpServer()).get(`${BASE}/999999`); + expect([404, 400]).toContain(res.status); + }); + + it(`POST ${BASE} (invalid payload) → 400`, async () => { + const res = await request(app.getHttpServer()).post(BASE).send(makeInvalidExpense()); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); + + it(`DELETE ${BASE}/:id → 200/204`, async () => { + let id = createdId; + if (!id) { + const created = await request(app.getHttpServer()) + .post(BASE) + .send(makeExpense(tsId, bcExpenseId)); + expect(created.status).toBe(201); + id = created.body.id; + } + + const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`); + expect([200, 204]).toContain(del.status); + if (createdId === id) createdId = null; }); }); diff --git a/test/factories/customer.factory.ts b/test/factories/customer.factory.ts deleted file mode 100644 index e69de29..0000000 diff --git a/test/factories/expense.factory.ts b/test/factories/expense.factory.ts index e69de29..6791afe 100644 --- a/test/factories/expense.factory.ts +++ b/test/factories/expense.factory.ts @@ -0,0 +1,37 @@ +// test/factories/expense.factory.ts +export type ExpensePayload = { + timesheet_id: number; + bank_code_id: number; // bank code categorie='EXPENSE' and type != 'MILEAGE' + date: string; // ISO date + amount: number; // INT (DTO: @IsInt) + description: string; + is_approved?: boolean; + supervisor_comment?: string; +}; + +const randInt = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1)) + min; + +const isoDate = (y: number, m: number, d: number) => + new Date(Date.UTC(y, m - 1, d, 0, 0, 0)).toISOString(); + +export function makeExpense( + tsId: number, + bcId: number, + overrides: Partial = {} +): ExpensePayload { + return { + timesheet_id: tsId, + bank_code_id: bcId, + date: isoDate(2024, 6, randInt(1, 28)), + amount: randInt(5, 200), + description: `Expense ${randInt(100, 999)}`, + supervisor_comment: 'N/A', + ...overrides, + }; +} + +// volontairement invalide pour 400 via ValidationPipe +export function makeInvalidExpense(): Record { + return { amount: 'oops', timesheet_id: 'nope' }; +} diff --git a/test/factories/shift.factory.ts b/test/factories/shift.factory.ts index e69de29..ef6e833 100644 --- a/test/factories/shift.factory.ts +++ b/test/factories/shift.factory.ts @@ -0,0 +1,43 @@ +// test/factories/shift.factory.ts +export type ShiftPayload = { + timesheet_id: number; + bank_code_id: number; + date: string; // ISO date (jour) + start_time: string; // ISO datetime (heure) + end_time: string; // ISO datetime (heure) + description: string; // requis par le DTO +}; + +const randInt = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1)) + min; + +const isoDate = (y: number, m: number, d: number) => + new Date(Date.UTC(y, m - 1, d, 0, 0, 0)).toISOString(); + +const isoTime = (h: number, m = 0) => + new Date(Date.UTC(1970, 0, 1, h, m, 0)).toISOString(); + +export function makeShift( + tsId: number, + bcId: number, + overrides: Partial = {} +): ShiftPayload { + // 8h pile pour ne pas dépasser DAILY_LIMIT_HOURS (8 par défaut) + const startH = 8; + const endH = 16; + + return { + timesheet_id: tsId, + bank_code_id: bcId, + date: isoDate(2024, 5, randInt(1, 28)), + start_time: isoTime(startH), + end_time: isoTime(endH), + description: `Shift ${randInt(100, 999)}`, + ...overrides, + }; +} + +// payload invalide pour 400 via ValidationPipe +export function makeInvalidShift(): Record { + return { timesheet_id: 'nope' }; // types volontairement faux +} diff --git a/test/factories/timesheet.factory.ts b/test/factories/timesheet.factory.ts index e69de29..dbcae3c 100644 --- a/test/factories/timesheet.factory.ts +++ b/test/factories/timesheet.factory.ts @@ -0,0 +1,39 @@ +// test/factories/timesheet.factory.ts + +export type TimesheetPayload = { + employee_id: number; + is_approved?: boolean; +}; + +/** + * Construit un payload valide pour POST /timesheets. + * Par défaut, is_approved=false. + */ +export function makeTimesheet( + employeeId: number, + overrides: Partial = {} +): TimesheetPayload { + return { + employee_id: employeeId, + is_approved: false, + ...overrides, + }; +} + +/** + * Payload délibérément invalide pour déclencher un 400 via ValidationPipe. + */ +export function makeInvalidTimesheet(): Record { + return { employee_id: 'not-a-number' }; +} + +/** + * Helper pour récupérer un employee_id existant + * sans importer les types Prisma dans le factory. + */ +export async function pickAnyEmployeeId( + prisma: { employees: { findFirst: (args?: any) => Promise<{ id: number } | null> } } +): Promise { + const emp = await prisma.employees.findFirst({ select: { id: true } }); + return emp?.id ?? null; +} diff --git a/test/shifts.e2e-spec.ts b/test/shifts.e2e-spec.ts index e054d18..2b26c44 100644 --- a/test/shifts.e2e-spec.ts +++ b/test/shifts.e2e-spec.ts @@ -1,25 +1,121 @@ import * as request from 'supertest'; import { INestApplication } from '@nestjs/common'; import { createApp } from './utils/testing-app'; -// import { resetDb } from './utils/reset-db'; import { PrismaService } from 'src/prisma/prisma.service'; +import { makeShift, makeInvalidShift } from './factories/shift.factory'; -describe('Shifts (e2e)', () => { +const BASE = '/shifts'; + +describe('Shifts (e2e) — autonome', () => { let app: INestApplication; - const BASE = '/shifts'; + let prisma: PrismaService; + let createdId: number | null = null; + + // FKs existants + let tsId: number; // any timesheet + let bcShiftId: number; // any bank code with categorie='SHIFT' + + beforeAll(async () => { + app = await createApp(); + prisma = app.get(PrismaService); + + // récupère un timesheet existant + const ts = await prisma.timesheets.findFirst({ select: { id: true } }); + if (!ts) throw new Error('No timesheet found — seed timesheets first.'); + tsId = ts.id; + + // récupère un bank code SHIFT (ta seed utilise 'SHIFT' en MAJ) + const bc = await prisma.bankCodes.findFirst({ + where: { categorie: 'SHIFT' }, + select: { id: true }, + }); + if (!bc) throw new Error('No SHIFT bank code found — seed bank codes first.'); + bcShiftId = bc.id; + }); - beforeAll(async () => { app = await createApp(); }); - beforeEach(async () => { - // await resetDb(app); - }); afterAll(async () => { - const prisma = app.get(PrismaService); + if (createdId) { + try { + await prisma.shifts.delete({ where: { id: createdId } }); + } catch {} + } await app.close(); await prisma.$disconnect(); }); - it(`GET ${BASE} → 200`, async () => { - const res = await request(app.getHttpServer()).get(BASE); - expect(res.status).toBe(200); + it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => { + const payload = makeShift(tsId, bcShiftId); + + const createRes = await request(app.getHttpServer()) + .post(BASE) + .send(payload); + + if (createRes.status !== 201) { + console.log('Create error:', createRes.body || createRes.text); + } + expect(createRes.status).toBe(201); + expect(createRes.body).toEqual( + expect.objectContaining({ + id: expect.any(Number), + timesheet_id: tsId, + bank_code_id: bcShiftId, + }) + ); + createdId = createRes.body.id; + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); + expect(getRes.status).toBe(200); + expect(getRes.body).toEqual(expect.objectContaining({ id: createdId })); + }); + + it(`PATCH ${BASE}/:id → 200 (description mise à jour)`, async () => { + if (!createdId) { + const created = await request(app.getHttpServer()) + .post(BASE) + .send(makeShift(tsId, bcShiftId)); + expect(created.status).toBe(201); + createdId = created.body.id; + } + + const patchRes = await request(app.getHttpServer()) + .patch(`${BASE}/${createdId}`) + .send({ description: 'Updated shift description' }); + + expect([200, 204]).toContain(patchRes.status); + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); + expect(getRes.status).toBe(200); + // on tolère l’absence, mais si elle est là, on vérifie la valeur. + if (getRes.body?.description !== undefined) { + expect(getRes.body.description).toBe('Updated shift description'); + } + }); + + it(`GET ${BASE}/999999 (not found) → 404/400`, async () => { + const res = await request(app.getHttpServer()).get(`${BASE}/999999`); + expect([404, 400]).toContain(res.status); + }); + + it(`POST ${BASE} (invalid payload) → 400`, async () => { + const res = await request(app.getHttpServer()) + .post(BASE) + .send(makeInvalidShift()); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); + + it(`DELETE ${BASE}/:id → 200/204`, async () => { + let id = createdId; + if (!id) { + const created = await request(app.getHttpServer()) + .post(BASE) + .send(makeShift(tsId, bcShiftId)); + expect(created.status).toBe(201); + id = created.body.id; + } + + const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`); + expect([200, 204]).toContain(del.status); + if (createdId === id) createdId = null; }); }); diff --git a/test/timesheets.e2e-spec.ts b/test/timesheets.e2e-spec.ts index 63f05cb..14dbc3d 100644 --- a/test/timesheets.e2e-spec.ts +++ b/test/timesheets.e2e-spec.ts @@ -1,17 +1,34 @@ -import * as request from 'supertest'; +// test/timesheets.e2e-spec.ts +const request = require('supertest'); import { INestApplication } from '@nestjs/common'; -import { createApp } from './utils/testing-app'; -// import { resetDb } from './utils/reset-db'; import { PrismaService } from 'src/prisma/prisma.service'; +import { + makeTimesheet, + makeInvalidTimesheet, +} from './factories/timesheet.factory'; +import { makeEmployee } from './factories/employee.factory'; +import { createApp } from './utils/testing-app'; -describe('Timesheets (e2e)', () => { - let app: INestApplication; +describe('Timesheets (e2e) — autonome', () => { const BASE = '/timesheets'; + let app: INestApplication; + let employeeIdForCreation: number; + + beforeAll(async () => { + app = await createApp(); + + // ✅ Crée un employé dédié pour cette suite + const empRes = await request(app.getHttpServer()) + .post('/employees') + .send(makeEmployee()); + if (empRes.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Impossible de créer un employé pour les tests timesheets:', empRes.body || empRes.text); + throw new Error('Setup employé échoué'); + } + employeeIdForCreation = empRes.body.id; + }); - beforeAll(async () => { app = await createApp(); }); - beforeEach(async () => { - // await resetDb(app); - }); afterAll(async () => { const prisma = app.get(PrismaService); await app.close(); @@ -21,5 +38,75 @@ describe('Timesheets (e2e)', () => { it(`GET ${BASE} → 200`, async () => { const res = await request(app.getHttpServer()).get(BASE); expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => { + const payload = makeTimesheet(employeeIdForCreation, { is_approved: true }); + + const createRes = await request(app.getHttpServer()).post(BASE).send(payload); + if (createRes.status !== 201) { + // eslint-disable-next-line no-console + console.log('Create error:', createRes.body || createRes.text); + } + expect(createRes.status).toBe(201); + expect(createRes.body).toEqual( + expect.objectContaining({ + id: expect.any(Number), + employee_id: payload.employee_id, + }), + ); + const id = createRes.body.id; + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${id}`); + expect(getRes.status).toBe(200); + expect(getRes.body).toEqual( + expect.objectContaining({ + id, + employee_id: payload.employee_id, + }), + ); + }); + + it(`PATCH ${BASE}/:id → 200 (is_approved toggled)`, async () => { + const created = await request(app.getHttpServer()) + .post(BASE) + .send(makeTimesheet(employeeIdForCreation)); + expect(created.status).toBe(201); + const id = created.body.id; + + const updated = await request(app.getHttpServer()) + .patch(`${BASE}/${id}`) + .send({ is_approved: true }); + expect(updated.status).toBe(200); + expect(updated.body).toEqual( + expect.objectContaining({ + id, + is_approved: true, + }), + ); + }); + + it(`POST ${BASE} (invalid payload) → 400`, async () => { + const bad = await request(app.getHttpServer()) + .post(BASE) + .send(makeInvalidTimesheet()); + + expect(bad.status).toBeGreaterThanOrEqual(400); + expect(bad.status).toBeLessThan(500); + }); + + it(`DELETE ${BASE}/:id → 200/204`, async () => { + const created = await request(app.getHttpServer()) + .post(BASE) + .send(makeTimesheet(employeeIdForCreation)); + expect(created.status).toBe(201); + const id = created.body.id; + + const delRes = await request(app.getHttpServer()).delete(`${BASE}/${id}`); + expect([200, 204]).toContain(delRes.status); + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${id}`); + expect(getRes.status).toBe(404); }); });