feat(tests): setup e2e-spec files for route testing. shifts, expenses, timesheets
This commit is contained in:
parent
fd3b9334e3
commit
1949731773
|
|
@ -835,11 +835,24 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "ShiftsController_getSummary",
|
"operationId": "ShiftsController_findAll",
|
||||||
"parameters": [],
|
"parameters": [],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"201": {
|
||||||
"description": ""
|
"description": "List of shifts found",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/CreateShiftDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "List of shifts not found"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -847,6 +860,7 @@
|
||||||
"access-token": []
|
"access-token": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"summary": "Find all shifts",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Shifts"
|
"Shifts"
|
||||||
]
|
]
|
||||||
|
|
@ -1003,6 +1017,25 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/shifts/summary": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "ShiftsController_getSummary",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"access-token": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Shifts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/shifts/export.csv": {
|
"/shifts/export.csv": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "ShiftsController_exportCsv",
|
"operationId": "ShiftsController_exportCsv",
|
||||||
|
|
@ -2361,7 +2394,6 @@
|
||||||
"description": "ID number of an bank code (link with bank-codes)"
|
"description": "ID number of an bank code (link with bank-codes)"
|
||||||
},
|
},
|
||||||
"date": {
|
"date": {
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "3018-10-20T00:00:00.000Z",
|
"example": "3018-10-20T00:00:00.000Z",
|
||||||
"description": "Date where the expense was made"
|
"description": "Date where the expense was made"
|
||||||
|
|
@ -2417,7 +2449,6 @@
|
||||||
"description": "ID number of an bank code (link with bank-codes)"
|
"description": "ID number of an bank code (link with bank-codes)"
|
||||||
},
|
},
|
||||||
"date": {
|
"date": {
|
||||||
"format": "date-time",
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"example": "3018-10-20T00:00:00.000Z",
|
"example": "3018-10-20T00:00:00.000Z",
|
||||||
"description": "Date where the expense was made"
|
"description": "Date where the expense was made"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
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 {
|
export class CreateExpenseDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 1,
|
example: 1,
|
||||||
description: 'Unique ID of the expense (auto-generated)',
|
description: 'Unique ID of the expense (auto-generated)',
|
||||||
})
|
})
|
||||||
id: number;
|
@Allow()
|
||||||
|
id?: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 101,
|
example: 101,
|
||||||
|
|
@ -31,17 +31,15 @@ export class CreateExpenseDto {
|
||||||
description: 'Date where the expense was made',
|
description: 'Date where the expense was made',
|
||||||
})
|
})
|
||||||
@IsDateString()
|
@IsDateString()
|
||||||
@Type(() => Date)
|
date: string;
|
||||||
@IsDate()
|
|
||||||
date: Date;
|
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 17.82,
|
example: 17.82,
|
||||||
description: 'amount in $ for a refund',
|
description: 'amount in $ for a refund',
|
||||||
})
|
})
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsNumber()
|
||||||
amount: number
|
amount: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example:'Spent for mileage between A and B',
|
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'
|
description:'Supervisro`s justification for the spending of an employee'
|
||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
supervisor_comment?: string;
|
supervisor_comment?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ export class ShiftsController {
|
||||||
return this.shiftsApprovalService.updateApproval(id, isApproved);
|
return this.shiftsApprovalService.updateApproval(id, isApproved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get('summary')
|
||||||
async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
|
async getSummary( @Query() query: GetShiftsOverviewDto): Promise<OverviewRow[]> {
|
||||||
return this.shiftsValidationService.getSummary(query.period_id);
|
return this.shiftsValidationService.getSummary(query.period_id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
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 {
|
export class CreateShiftDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 1,
|
example: 1,
|
||||||
description: 'Unique ID of the shift (auto-generated)',
|
description: 'Unique ID of the shift (auto-generated)',
|
||||||
})
|
})
|
||||||
|
@Allow()
|
||||||
id: number;
|
id: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,11 @@ export class ShiftsQueryService {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(dto: CreateShiftDto): Promise<Shifts> {
|
async create(dto: CreateShiftDto): Promise<Shifts> {
|
||||||
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
|
//shift creation
|
||||||
const shift = await this.prisma.shifts.create({
|
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 } } } },
|
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||||
bank_code: true,
|
bank_code: true,
|
||||||
},
|
},
|
||||||
|
|
@ -92,7 +92,7 @@ export class ShiftsQueryService {
|
||||||
|
|
||||||
async update(id: number, dto: UpdateShiftsDto): Promise<Shifts> {
|
async update(id: number, dto: UpdateShiftsDto): Promise<Shifts> {
|
||||||
await this.findOne(id);
|
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({
|
return this.prisma.shifts.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -101,6 +101,7 @@ export class ShiftsQueryService {
|
||||||
...(date !== undefined && { date }),
|
...(date !== undefined && { date }),
|
||||||
...(start_time !== undefined && { start_time }),
|
...(start_time !== undefined && { start_time }),
|
||||||
...(end_time !== undefined && { end_time }),
|
...(end_time !== undefined && { end_time }),
|
||||||
|
...(description !== undefined && { description }),
|
||||||
},
|
},
|
||||||
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
include: { timesheet: { include: { employee: { include: { user: true } } } },
|
||||||
bank_code: true,
|
bank_code: true,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Type } from "class-transformer";
|
||||||
import { IsBoolean, IsInt, IsOptional } from "class-validator";
|
import { Allow, IsBoolean, IsInt, IsOptional } from "class-validator";
|
||||||
|
|
||||||
export class CreateTimesheetDto {
|
export class CreateTimesheetDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 1,
|
example: 1,
|
||||||
description: 'timesheet`s unique ID (auto-generated)',
|
description: 'timesheet`s unique ID (auto-generated)',
|
||||||
})
|
})
|
||||||
id: number;
|
@Allow()
|
||||||
|
id?: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 426433,
|
example: 426433,
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,120 @@
|
||||||
import * as request from 'supertest';
|
import * as request from 'supertest';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { createApp } from './utils/testing-app';
|
import { createApp } from './utils/testing-app';
|
||||||
// import { resetDb } from './utils/reset-db';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
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;
|
let app: INestApplication;
|
||||||
const BASE = '/expenses';
|
let prisma: PrismaService;
|
||||||
|
let createdId: number | null = null;
|
||||||
|
|
||||||
beforeAll(async () => { app = await createApp(); });
|
let tsId: number;
|
||||||
beforeEach(async () => {
|
let bcExpenseId: number; // categorie='EXPENSE' & type != 'MILEAGE'
|
||||||
// await resetDb(app);
|
|
||||||
|
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 () => {
|
afterAll(async () => {
|
||||||
const prisma = app.get(PrismaService);
|
if (createdId) {
|
||||||
|
try { await prisma.expenses.delete({ where: { id: createdId } }); } catch {}
|
||||||
|
}
|
||||||
await app.close();
|
await app.close();
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`GET ${BASE} → 200`, async () => {
|
it(`GET ${BASE} → 200 (array)`, async () => {
|
||||||
const res = await request(app.getHttpServer()).get(BASE);
|
const res = await request(app.getHttpServer()).get(BASE);
|
||||||
expect(res.status).toBe(200);
|
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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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> = {}
|
||||||
|
): 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<string, unknown> {
|
||||||
|
return { amount: 'oops', timesheet_id: 'nope' };
|
||||||
|
}
|
||||||
|
|
@ -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> = {}
|
||||||
|
): 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<string, unknown> {
|
||||||
|
return { timesheet_id: 'nope' }; // types volontairement faux
|
||||||
|
}
|
||||||
|
|
@ -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> = {}
|
||||||
|
): 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<string, unknown> {
|
||||||
|
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<number | null> {
|
||||||
|
const emp = await prisma.employees.findFirst({ select: { id: true } });
|
||||||
|
return emp?.id ?? null;
|
||||||
|
}
|
||||||
|
|
@ -1,25 +1,121 @@
|
||||||
import * as request from 'supertest';
|
import * as request from 'supertest';
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { createApp } from './utils/testing-app';
|
import { createApp } from './utils/testing-app';
|
||||||
// import { resetDb } from './utils/reset-db';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
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;
|
let app: INestApplication;
|
||||||
const BASE = '/shifts';
|
let prisma: PrismaService;
|
||||||
|
let createdId: number | null = null;
|
||||||
|
|
||||||
beforeAll(async () => { app = await createApp(); });
|
// FKs existants
|
||||||
beforeEach(async () => {
|
let tsId: number; // any timesheet
|
||||||
// await resetDb(app);
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
const prisma = app.get(PrismaService);
|
if (createdId) {
|
||||||
|
try {
|
||||||
|
await prisma.shifts.delete({ where: { id: createdId } });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
await app.close();
|
await app.close();
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`GET ${BASE} → 200`, async () => {
|
it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => {
|
||||||
const res = await request(app.getHttpServer()).get(BASE);
|
const payload = makeShift(tsId, bcShiftId);
|
||||||
expect(res.status).toBe(200);
|
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,34 @@
|
||||||
import * as request from 'supertest';
|
// test/timesheets.e2e-spec.ts
|
||||||
|
const request = require('supertest');
|
||||||
import { INestApplication } from '@nestjs/common';
|
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 { 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)', () => {
|
describe('Timesheets (e2e) — autonome', () => {
|
||||||
let app: INestApplication;
|
|
||||||
const BASE = '/timesheets';
|
const BASE = '/timesheets';
|
||||||
|
let app: INestApplication;
|
||||||
|
let employeeIdForCreation: number;
|
||||||
|
|
||||||
beforeAll(async () => { app = await createApp(); });
|
beforeAll(async () => {
|
||||||
beforeEach(async () => {
|
app = await createApp();
|
||||||
// await resetDb(app);
|
|
||||||
|
// ✅ 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;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
const prisma = app.get(PrismaService);
|
const prisma = app.get(PrismaService);
|
||||||
await app.close();
|
await app.close();
|
||||||
|
|
@ -21,5 +38,75 @@ describe('Timesheets (e2e)', () => {
|
||||||
it(`GET ${BASE} → 200`, async () => {
|
it(`GET ${BASE} → 200`, async () => {
|
||||||
const res = await request(app.getHttpServer()).get(BASE);
|
const res = await request(app.getHttpServer()).get(BASE);
|
||||||
expect(res.status).toBe(200);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user