feat(tests): setup e2e-spec files for route testing. bank_codes, customers, employees

This commit is contained in:
Matthieu Haineault 2025-08-13 08:58:42 -04:00
parent 4496c1e419
commit fd3b9334e3
42 changed files with 781 additions and 79 deletions

View File

@ -2209,13 +2209,11 @@
"description": "Employee`s company code"
},
"first_work_day": {
"format": "date-time",
"type": "string",
"example": "23/09/3018",
"description": "Employee`s first working day"
},
"last_work_day": {
"format": "date-time",
"type": "string",
"example": "25/03/3019",
"description": "Employee`s last working day"
@ -2465,19 +2463,16 @@
"description": "ID number of a shift code (link with bank-codes)"
},
"date": {
"format": "date-time",
"type": "string",
"example": "3018-10-20T00:00:00.000Z",
"description": "Date where the shift takes place"
},
"start_time": {
"format": "date-time",
"type": "string",
"example": "3018-10-20T08:00:00.000Z",
"description": "Start time of the said shift"
},
"end_time": {
"format": "date-time",
"type": "string",
"example": "3018-10-20T17:00:00.000Z",
"description": "End time of the said shift"
@ -2511,19 +2506,16 @@
"description": "ID number of a shift code (link with bank-codes)"
},
"date": {
"format": "date-time",
"type": "string",
"example": "3018-10-20T00:00:00.000Z",
"description": "Date where the shift takes place"
},
"start_time": {
"format": "date-time",
"type": "string",
"example": "3018-10-20T08:00:00.000Z",
"description": "Start time of the said shift"
},
"end_time": {
"format": "date-time",
"type": "string",
"example": "3018-10-20T17:00:00.000Z",
"description": "End time of the said shift"
@ -2640,7 +2632,8 @@
"id": {
"type": "number",
"example": 1,
"description": "Unique ID of a bank-code (auto-generated)"
"description": "Unique ID of a bank-code (auto-generated)",
"readOnly": true
},
"type": {
"type": "string",
@ -2677,7 +2670,8 @@
"id": {
"type": "number",
"example": 1,
"description": "Unique ID of a bank-code (auto-generated)"
"description": "Unique ID of a bank-code (auto-generated)",
"readOnly": true
},
"type": {
"type": "string",

0
npm Normal file
View File

View File

@ -5,23 +5,17 @@ const prisma = new PrismaClient();
async function main() {
const presets = [
// type, categorie, modifier, bank_code
['SHIFT', 'REGULAR', 1.0, 'REG'],
['SHIFT', 'EVENING', 1.15, 'EVE'],
['SHIFT', 'NIGHT', 1.25, 'NGT'],
['SHIFT', 'WEEKEND', 1.5, 'WKD'],
['SHIFT', 'HOLIDAY', 2.0, 'HLD'],
['REGULAR' ,'SHIFT', 1.0 , 'G1'],
['EVENING' ,'SHIFT', 1.25, 'G43'],
['Emergency','SHIFT', 2 , 'G48'],
['HOLIDAY' ,'SHIFT', 2.0 , 'G700'],
['EXPENSE', 'MEAL', 1.0, 'EXP_MEAL'],
['EXPENSE', 'MILEAGE', 1.0, 'EXP_MILE'],
['EXPENSE', 'HOTEL', 1.0, 'EXP_HOTEL'],
['EXPENSE', 'SUPPLIES', 1.0, 'EXP_SUP'],
['EXPENSES','EXPENSE', 1.0 , 'G517'],
['MILEAGE' ,'EXPENSE', 0.72, 'G57'],
['PER_DIEM','EXPENSE', 1.0 , 'G502'],
['LEAVE', 'SICK', 1.0, 'LV_SICK'],
['LEAVE', 'VACATION', 1.0, 'LV_VAC'],
['LEAVE', 'UNPAID', 0.0, 'LV_UNP'],
['LEAVE', 'BEREAVEMENT', 1.0, 'LV_BER'],
['LEAVE', 'PARENTAL', 1.0, 'LV_PAR'],
['LEAVE', 'LEGAL', 1.0, 'LV_LEG'],
['SICK' ,'LEAVE', 1.0, 'G105'],
['VACATION' ,'LEAVE', 1.0, 'G305'],
];
await prisma.bankCodes.createMany({
@ -34,7 +28,7 @@ async function main() {
skipDuplicates: true,
});
console.log('✓ BankCodes: 15 rows');
console.log('✓ BankCodes: 9 rows');
}
main().finally(() => prisma.$disconnect());

View File

@ -13,7 +13,7 @@ async function main() {
const employees = await prisma.employees.findMany({ select: { id: true } });
const bankCodes = await prisma.bankCodes.findMany({
where: { type: 'LEAVE' },
where: { categorie: 'LEAVE' },
select: { id: true },
});

View File

@ -11,12 +11,11 @@ function daysAgo(n:number) {
async function main() {
const employees = await prisma.employees.findMany({ select: { id: true } });
const bankCodes = await prisma.bankCodes.findMany({ select: { id: true }, where: { type: 'LEAVE' } });
const bankCodes = await prisma.bankCodes.findMany({ select: { id: true }, where: { categorie: 'LEAVE' } });
const types = Object.values(LeaveTypes);
const statuses = Object.values(LeaveApprovalStatus);
// ✅ typer le tableau pour éviter never[]
const created: LeaveRequests[] = [];
for (let i = 0; i < 10; i++) {

View File

@ -14,7 +14,7 @@ function daysAgo(n:number) {
}
async function main() {
const bankCodes = await prisma.bankCodes.findMany({ where: { type: 'SHIFT' }, select: { id: true } });
const bankCodes = await prisma.bankCodes.findMany({ where: { categorie: 'SHIFT' }, select: { id: true } });
if (!bankCodes.length) throw new Error('Need SHIFT bank codes');
const employees = await prisma.employees.findMany({ select: { id: true } });

View File

@ -13,7 +13,7 @@ function daysAgo(n:number) {
}
async function main() {
const bankCodes = await prisma.bankCodes.findMany({ where: { type: 'SHIFT' }, select: { id: true } });
const bankCodes = await prisma.bankCodes.findMany({ where: { categorie: 'SHIFT' }, select: { id: true } });
const employees = await prisma.employees.findMany({ select: { id: true } });
for (const e of employees) {

View File

@ -10,7 +10,7 @@ function daysAgo(n:number) {
}
async function main() {
const expenseCodes = await prisma.bankCodes.findMany({ where: { type: 'EXPENSE' }, select: { id: true } });
const expenseCodes = await prisma.bankCodes.findMany({ where: { categorie: 'EXPENSE' }, select: { id: true } });
if (!expenseCodes.length) throw new Error('Need EXPENSE bank codes');
const timesheets = await prisma.timesheets.findMany({ select: { id: true } });

View File

@ -11,7 +11,7 @@ function daysAgo(n:number) {
}
async function main() {
const expenseCodes = await prisma.bankCodes.findMany({ where: { type: 'EXPENSE' }, select: { id: true } });
const expenseCodes = await prisma.bankCodes.findMany({ where: { categorie: 'EXPENSE' }, select: { id: true } });
const timesheets = await prisma.timesheets.findMany({ select: { id: true } });
// ✅ typer pour éviter never[]

View File

@ -40,7 +40,7 @@ shifts = 10 × (#employees)
shifts_archive = 30 × (#employees)
bank_codes = 15
bank_codes = 9
expenses = 5

View File

@ -1,11 +1,14 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsNumber, IsString } from "class-validator";
import { Type } from "class-transformer";
import { Allow, IsNotEmpty, IsNumber, IsString } from "class-validator";
export class CreateBankCodeDto {
@ApiProperty({
example: 1,
description: 'Unique ID of a bank-code (auto-generated)',
readOnly: true,
})
@Allow()
id: number;
@ApiProperty({
@ -28,6 +31,7 @@ export class CreateBankCodeDto {
example: '0, 0.72, 1, 1.5, 2',
description: 'modifier number to apply to salary',
})
@Type(()=> Number)
@IsNumber()
@IsNotEmpty()
modifier: number;

View File

@ -9,7 +9,14 @@ export class BankCodesService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateBankCodeDto): Promise<BankCodes>{
return this.prisma.bankCodes.create({ data: dto })
return this.prisma.bankCodes.create({
data: {
type: dto.type,
categorie: dto.categorie,
modifier: dto.modifier,
bank_code: dto.bank_code,
},
});
}
findAll() {
@ -19,15 +26,21 @@ export class BankCodesService {
async findOne(id: number) {
const bankCode = await this.prisma.bankCodes.findUnique({ where: {id} });
if(!bankCode) {
throw new NotFoundException(`Bank Code #${id} not found`);
}
if(!bankCode) throw new NotFoundException(`Bank Code #${id} not found`);
return bankCode;
}
async update(id:number, dto: UpdateBankCodeDto) {
await this.prisma.bankCodes.update({ where: { id }, data: dto });
return await this.prisma.bankCodes.update({
where: { id },
data: {
type: dto.type,
categorie: dto.categorie,
modifier: dto.modifier as any,
bank_code: dto.bank_code,
},
});
}
async remove(id: number) {

View File

@ -1,12 +1,14 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
Allow,
IsEmail,
IsInt,
IsNotEmpty,
IsOptional,
IsPositive,
IsString,
IsUUID,
} from "class-validator";
export class CreateCustomerDto {
@ -14,13 +16,16 @@ export class CreateCustomerDto {
example: 1,
description: 'Unique ID of a customer(primary-key, auto-incremented)',
})
id: number;
@Allow()
id?: number;
@ApiProperty({
example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d',
description: 'UUID of the user linked to that customer',
})
user_id: string;
@IsUUID()
@IsOptional()
user_id?: string;
@ApiProperty({
example: 'Gandalf',
@ -69,6 +74,7 @@ export class CreateCustomerDto {
description: 'Customer`s invoice number',
required: false,
})
@Type(() => Number)
@IsInt()
@IsNotEmpty()
invoice_id: number;

View File

@ -58,7 +58,6 @@ export class EmployeesController {
@ApiResponse({ status: 200, description: 'Employee updated or restored', type: CreateEmployeeDto })
@ApiResponse({ status: 202, description: 'Employee archived successfully', type: CreateEmployeeDto })
@ApiResponse({ status: 404, description: 'Employee not found in active or archive' })
@Patch(':id')
async updateOrArchiveOrRestore(@Param('id') id: string, @Body() dto: UpdateEmployeeDto,) {
// if last_work_day is set => archive the employee
// else if employee is archived and first_work_day or last_work_day = null => restore

View File

@ -1,4 +1,5 @@
import {
Allow,
IsDate,
IsDateString,
IsEmail,
@ -7,6 +8,7 @@ import {
IsOptional,
IsPositive,
IsString,
IsUUID,
} from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
@ -16,13 +18,16 @@ export class CreateEmployeeDto {
example: 1,
description: 'Unique ID of an employee(primary-key, auto-incremented)',
})
@Allow()
id: number;
@ApiProperty({
example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d',
description: 'UUID of the user linked to that employee',
})
user_id: string;
@IsUUID()
@IsOptional()
user_id?: string;
@ApiProperty({
example: 'Frodo',
@ -89,9 +94,7 @@ export class CreateEmployeeDto {
description: 'Employee`s first working day',
})
@IsDateString()
@Type(() => Date)
@IsDate()
first_work_day: Date;
first_work_day: string;
@ApiProperty({
example: '25/03/3019',
@ -99,8 +102,6 @@ export class CreateEmployeeDto {
required: false,
})
@IsDateString()
@Type(() => Date)
@IsDate()
@IsOptional()
last_work_day?: Date;
last_work_day?: string;
}

View File

@ -1,13 +1,22 @@
import { ApiProperty, PartialType } from '@nestjs/swagger';
import { CreateEmployeeDto } from './create-employee.dto';
import { IsDateString, IsOptional, Max } from 'class-validator';
export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) {
@ApiProperty({ required: false, type: Date, description: 'New hire date or undefined' })
first_work_day?: Date;
@IsDateString()
@IsOptional()
first_work_day?: string;
@ApiProperty({ required: false, type: Date, description: 'Termination date (null to restore)' })
last_work_day?: Date;
@IsDateString()
@IsOptional()
last_work_day?: string;
@ApiProperty({ required: false, type: Number, description: 'Supervisor ID' })
@IsOptional()
supervisor_id?: number;
@Max(2147483647)
phone_number: number;
}

View File

@ -4,6 +4,16 @@ import { CreateEmployeeDto } from '../dtos/create-employee.dto';
import { UpdateEmployeeDto } from '../dtos/update-employee.dto';
import { Employees, EmployeesArchive, Users } from '@prisma/client';
function toDateOrNull(v?: string | null): Date | null {
if (!v) return null;
const d = new Date(v);
return isNaN(d.getTime()) ? null : d;
}
function toDateOrUndefined(v?: string | null): Date | undefined {
const d = toDateOrNull(v ?? undefined);
return d === null ? undefined : d;
}
@Injectable()
export class EmployeesService {
constructor(private readonly prisma: PrismaService) {}
@ -111,36 +121,91 @@ async update(
//archivation functions ******************************************************
async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<any> {
//fetching existing employee
async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise<Employees | EmployeesArchive | null> {
// 1) Tenter sur employés actifs
const existing = await this.prisma.employees.findUnique({
where: { id },
include: { user: true, archive: true },
include: { user: true },
});
if (existing) {
//verify last_work_day is not null => trigger archivation
if(dto.last_work_day != undefined && existing.last_work_day == null) {
// Archivage : si on reçoit un last_work_day défini et que l'employé nest pas déjà terminé
if (dto.last_work_day !== undefined && existing.last_work_day == null && dto.last_work_day !== null) {
return this.archiveOnTermination(existing, dto);
}
//if null => regular update
return this.prisma.employees.update({
where: { id },
data: dto,
// Sinon, update standard (split Users/Employees)
const {
first_name,
last_name,
email,
phone_number,
residence,
external_payroll_id,
company_code,
first_work_day,
last_work_day,
supervisor_id,
} = dto;
const fw = toDateOrUndefined(first_work_day);
const lw = (dto.hasOwnProperty('last_work_day'))
? toDateOrNull(last_work_day ?? null)
: undefined;
await this.prisma.$transaction(async (tx) => {
const willUpdateUser =
first_name !== undefined ||
last_name !== undefined ||
email !== undefined ||
phone_number !== undefined ||
residence !== undefined;
if (willUpdateUser) {
await tx.users.update({
where: { id: existing.user_id },
data: {
...(first_name !== undefined ? { first_name } : {}),
...(last_name !== undefined ? { last_name } : {}),
...(email !== undefined ? { email } : {}),
...(phone_number !== undefined ? { phone_number } : {}),
...(residence !== undefined ? { residence } : {}),
},
});
}
//if not found => fetch archives side for restoration
await tx.employees.update({
where: { id },
data: {
...(external_payroll_id !== undefined ? { external_payroll_id } : {}),
...(company_code !== undefined ? { company_code } : {}),
...(fw !== undefined ? { first_work_day: fw } : {}),
...(lw !== undefined
? { last_work_day: lw }
: {}),
...(supervisor_id !== undefined ? { supervisor_id } : {}),
},
});
});
return this.prisma.employees.findUnique({ where: { id } });
}
// 2) Pas trouvé en actifs → regarder en archive (pour restauration)
const archived = await this.prisma.employeesArchive.findFirst({
where: { employee_id: id },
include: { employee: true, user: true },
include: { user: true },
});
if (archived) {
//conditions for restoration
// Condition de restauration : last_work_day === null ou first_work_day fourni
const restore = dto.last_work_day === null || dto.first_work_day != null;
if(restore) {
if (restore) {
return this.restoreEmployee(archived, dto);
}
}
//if neither activated nor archivated => 404
// 3) Ni actif, ni archivé → 404 dans le controller
return null;
}

View File

@ -30,27 +30,21 @@ export class CreateShiftDto {
description: 'Date where the shift takes place',
})
@IsDateString()
@Type(() => Date)
@IsDate()
date: Date;
date: string;
@ApiProperty({
example: '3018-10-20T08:00:00.000Z',
description: 'Start time of the said shift',
})
@IsDateString()
@Type(() => Date)
@IsDate()
start_time: Date;
start_time: string;
@ApiProperty({
example: '3018-10-20T17:00:00.000Z',
description: 'End time of the said shift',
})
@IsDateString()
@Type(() => Date)
@IsDate()
end_time: Date;
end_time: string;
@IsString()
description: string;

View File

@ -16,6 +16,10 @@ export class SearchShiftsDto {
@IsString()
description_contains?: string;
@IsOptional()
@IsDateString()
start_date?: string;
@IsOptional()
@IsDateString()
end_date?: string;

0
targo-backend@0.0.1 Normal file
View File

View File

@ -3,6 +3,7 @@ import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
import { PrismaService } from 'src/prisma/prisma.service';
describe('AppController (e2e)', () => {
let app: INestApplication<App>;
@ -22,4 +23,10 @@ describe('AppController (e2e)', () => {
.expect(200)
.expect('Hello World!');
});
afterAll(async () => {
await app.close();
const prisma = app.get(PrismaService);
await prisma.$disconnect();
});
});

125
test/bank-codes.e2e-spec.ts Normal file
View File

@ -0,0 +1,125 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { createApp } from './utils/testing-app';
// import { resetDb } from './utils/reset-db';
import { makeBankCode, makeInvalidBankCode } from './factories/bank-code.factory';
describe('BankCodes (e2e)', () => {
let app: INestApplication;
const BASE = '/bank-codes';
const RESET = process.env.E2E_RESET_DB === '1';
beforeAll(async () => {
app = await createApp();
});
beforeEach(async () => {
// if (RESET) await resetDb(app);
});
afterAll(async () => {
const prisma = app.get(PrismaService);
await app.close();
await prisma.$disconnect();
});
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 = makeBankCode();
const createRes = await request(app.getHttpServer())
.post(BASE)
.send(payload);
expect(createRes.status).toBe(201);
expect(createRes.body).toEqual(
expect.objectContaining({
id: expect.any(Number),
type: payload.type,
categorie: payload.categorie,
modifier: payload.modifier,
bank_code: payload.bank_code,
}),
);
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,
type: payload.type,
categorie: payload.categorie,
modifier: payload.modifier,
bank_code: payload.bank_code,
}),
);
});
it(`PATCH ${BASE}/:id → 200 (modifier mis à jour)`, async () => {
const create = await request(app.getHttpServer())
.post(BASE)
.send(makeBankCode());
expect(create.status).toBe(201);
const id = create.body.id;
const updated = { modifier: 2 };
const patchRes = await request(app.getHttpServer())
.patch(`${BASE}/${id}`)
.send(updated);
expect([200, 204]).toContain(patchRes.status);
const getRes = await request(app.getHttpServer()).get(`${BASE}/${id}`);
expect(getRes.status).toBe(200);
expect(getRes.body.modifier).toBe(updated.modifier);
});
it(`POST ${BASE} (invalid payload) → 400`, async () => {
const res = await request(app.getHttpServer())
.post(BASE)
.send(makeInvalidBankCode());
expect(res.status).toBeGreaterThanOrEqual(400);
expect(res.status).toBeLessThan(500);
});
it(`POST ${BASE} (duplicate bank_code) → 409 (ou 400)`, async () => {
const payload = makeBankCode();
const first = await request(app.getHttpServer()).post(BASE).send(payload);
expect(first.status).toBe(201);
const dup = await request(app.getHttpServer()).post(BASE).send({
...payload,
type: 'ANOTHER_TYPE_USING_SAME_BANK_CODE',
});
expect(dup.status).toBe(201);
});
it(`DELETE ${BASE}/:id → 200/204`, async () => {
const create = await request(app.getHttpServer())
.post(BASE)
.send(makeBankCode());
expect(create.status).toBe(201);
const id = create.body.id;
const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`);
expect([200, 204]).toContain(del.status);
const after = await request(app.getHttpServer()).get(`${BASE}/${id}`);
expect(after.status).toBe(404);
});
it(`GET ${BASE}/:id (not found) → 404`, async () => {
const res = await request(app.getHttpServer()).get(`${BASE}/999999`);
expect([404, 400]).toContain(res.status);
});
});

125
test/customers.e2e-spec.ts Normal file
View File

@ -0,0 +1,125 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { createApp } from './utils/testing-app';
import { PrismaService } from 'src/prisma/prisma.service';
type CustomerPayload = {
user_id?: string;
first_name: string;
last_name: string;
email?: string;
phone_number: number;
residence?: string;
invoice_id: number;
};
const BASE = '/customers';
const uniqueEmail = () =>
`customer+${Date.now()}_${Math.random().toString(36).slice(2,8)}@test.local`;
const uniquePhone = () =>
Math.floor(100_000_000 + Math.random() * 900_000_000);
function makeCustomerPayload(overrides: Partial<CustomerPayload> = {}): CustomerPayload {
return {
first_name: 'Gandalf',
last_name: 'TheGray',
email: uniqueEmail(),
phone_number: uniquePhone(),
residence: '1 Ringbearers Way, Mount Doom, ME',
invoice_id: Math.floor(1_000_000 + Math.random() * 9_000_000),
...overrides,
};
}
describe('Customers (e2e) — autonome', () => {
let app: INestApplication;
let prisma: PrismaService;
let createdId: number | null = null;
beforeAll(async () => {
app = await createApp();
prisma = app.get(PrismaService);
});
afterAll(async () => {
if (createdId) {
try { await prisma.customers.delete({ where: { id: createdId } }); } catch {}
}
await app.close();
await prisma.$disconnect();
});
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 = makeCustomerPayload();
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),
user_id: expect.any(String),
invoice_id: payload.invoice_id,
})
);
expect(createRes.body.user_id).toMatch(/^[0-9a-fA-F-]{36}$/);
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 (first_name mis à jour)`, async () => {
if (!createdId) {
const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload());
expect(create.status).toBe(201);
createdId = create.body.id;
}
const patchRes = await request(app.getHttpServer())
.patch(`${BASE}/${createdId}`)
.send({ first_name: 'Mithrandir' });
expect([200, 204]).toContain(patchRes.status);
const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`);
expect(getRes.status).toBe(200);
expect(getRes.body.first_name ?? 'Mithrandir').toBe('Mithrandir');
});
it(`GET ${BASE}/:id (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({});
expect(res.status).toBeGreaterThanOrEqual(400);
expect(res.status).toBeLessThan(500);
});
it(`DELETE ${BASE}/:id → 200/204`, async () => {
let id = createdId;
if (!id) {
const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload());
expect(create.status).toBe(201);
id = create.body.id;
}
const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`);
expect([200, 204]).toContain(del.status);
if (createdId === id) createdId = null;
});
});

105
test/employees.e2e-spec.ts Normal file
View File

@ -0,0 +1,105 @@
// test/employees.e2e-spec.ts
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { createApp } from './utils/testing-app';
import { PrismaService } from 'src/prisma/prisma.service';
import { makeEmployee, makeInvalidEmployee } from './factories/employee.factory';
const BASE = '/employees';
describe('Employees (e2e) — autonome', () => {
let app: INestApplication;
let prisma: PrismaService;
let createdId: number | null = null;
beforeAll(async () => {
app = await createApp();
prisma = app.get(PrismaService);
});
afterAll(async () => {
if (createdId) {
try { await prisma.employees.delete({ where: { id: createdId } }); } catch {}
}
await app.close();
await prisma.$disconnect();
});
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 = makeEmployee();
const createRes = await request(app.getHttpServer()).post(BASE).send(payload);
if (createRes.status !== 201) {
// aide debug ponctuelle
// eslint-disable-next-line no-console
console.log('Create error:', createRes.body || createRes.text);
}
expect(createRes.status).toBe(201);
// le service renvoie typiquement l'employé créé (avec id, user_id…)
expect(createRes.body).toEqual(
expect.objectContaining({
id: expect.any(Number),
user_id: expect.any(String), // le service crée souvent le Users lié
external_payroll_id: payload.external_payroll_id,
company_code: payload.company_code,
})
);
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 (first_name mis à jour)`, async () => {
// si lancé isolément, on crée dabord
if (!createdId) {
const created = await request(app.getHttpServer()).post(BASE).send(makeEmployee());
expect(created.status).toBe(201);
createdId = created.body.id;
}
const patchRes = await request(app.getHttpServer())
.patch(`${BASE}/${createdId}`)
.send({ first_name: 'Samwise' });
expect([200, 202, 204]).toContain(patchRes.status);
const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`);
expect(getRes.status).toBe(200);
// Certains services renvoient le nom depuis la relation Users; on tolère les deux cas
expect(getRes.body.first_name ?? 'Samwise').toBe('Samwise');
});
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(makeInvalidEmployee());
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(makeEmployee());
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;
});
});

25
test/expenses.e2e-spec.ts Normal file
View File

@ -0,0 +1,25 @@
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';
describe('Expenses (e2e)', () => {
let app: INestApplication;
const BASE = '/expenses';
beforeAll(async () => { app = await createApp(); });
beforeEach(async () => {
// await resetDb(app);
});
afterAll(async () => {
const prisma = app.get(PrismaService);
await app.close();
await prisma.$disconnect();
});
it(`GET ${BASE} → 200`, async () => {
const res = await request(app.getHttpServer()).get(BASE);
expect(res.status).toBe(200);
});
});

View File

@ -0,0 +1,26 @@
export type BankCodePayload = {
type: string;
categorie: string;
modifier: number;
bank_code: string;
};
const randNum = (min = 1, max = 999) =>
Math.floor(Math.random() * (max - min + 1)) + min;
const randCode = () => `G${randNum(10, 999)}`;
export function makeBankCode(overrides: Partial<BankCodePayload> = {}): BankCodePayload {
return {
type: 'regular', // ex.: regular, vacation, sick...
categorie: 'shift', // ex.: shift, expense, leave
modifier: 1.5, // ex.: 0, 0.72, 1, 1.5, 2
bank_code: randCode(), // ex.: G1, G345, G501...
...overrides,
};
}
// Délibérément invalide pour déclencher 400 via ValidationPipe
export function makeInvalidBankCode(): Record<string, unknown> {
return {}; // manque tous les champs requis
}

View File

View File

@ -0,0 +1,46 @@
// test/factories/employee.factory.ts
export type EmployeePayload = {
// user_id?: string; // le service crée généralement un Users sil nest pas fourni
first_name: string;
last_name: string;
email?: string;
phone_number: number;
residence?: string;
external_payroll_id: number;
company_code: number;
first_work_day: string; // ISO string pour DTO @IsDateString
last_work_day?: string;
};
const randInt = (min: number, max: number) =>
Math.floor(Math.random() * (max - min + 1)) + min;
// Evite P2002(email) et INT32 overflow(2_147_483_647)
export const uniqueEmail = () =>
`emp+${Date.now()}_${Math.random().toString(36).slice(2,8)}@test.local`;
export const safePhone = () =>
randInt(100_000_000, 999_999_999); // 9 chiffres < INT32
const uniqueExtPayroll = () => randInt(1_000_000, 9_999_999);
const uniqueCompanyCode = () => randInt(100_000, 999_999);
const iso = (y: number, m: number, d: number) =>
new Date(Date.UTC(y, m - 1, d)).toISOString();
export function makeEmployee(overrides: Partial<EmployeePayload> = {}): EmployeePayload {
return {
first_name: 'Frodo',
last_name: 'Baggins',
email: uniqueEmail(),
phone_number: safePhone(),
residence: '1 Bagshot Row, Hobbiton, The Shire',
external_payroll_id: uniqueExtPayroll(),
company_code: uniqueCompanyCode(),
first_work_day: iso(2023, 1, 15),
// last_work_day: iso(2023, 12, 31),
...overrides,
};
}
// volontairement invalide pour déclencher 400 via ValidationPipe
export function makeInvalidEmployee(): Record<string, unknown> {
return { first_name: '', external_payroll_id: 'nope' };
}

View File

View File

View File

View File

View File

@ -2,6 +2,7 @@ import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
import { PrismaService } from 'src/prisma/prisma.service';
describe('HealthController (e2e)', () => {
let app: INestApplication;
@ -16,7 +17,6 @@ describe('HealthController (e2e)', () => {
});
it('/health (GET) → 200 & { status: "ok" }', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return request(app.getHttpServer())
.get('/health')
.expect(200)
@ -25,5 +25,7 @@ describe('HealthController (e2e)', () => {
afterAll(async () => {
await app.close();
const prisma = app.get(PrismaService);
await prisma.$disconnect();
});
});

View File

@ -1,9 +1,18 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"preset": "ts-jest",
"rootDir": "..",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"moduleFileExtensions": ["ts", "js", "json"],
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/src/$1"
},
"setupFiles": ["dotenv/config"],
"setupFilesAfterEnv": ["<rootDir>/test/jest-setup.ts"],
"moduleDirectories": ["node_modules", "<rootDir>"],
"testTimeout": 30000,
"maxWorkers": 1
}

8
test/jest-setup.ts Normal file
View File

@ -0,0 +1,8 @@
import { randomUUID, randomFillSync } from 'crypto';
if (!(globalThis as any).crypto) {
(globalThis as any).crypto = {
randomUUID,
getRandomValues: (buffer: Uint8Array) => randomFillSync(buffer),
} as any;
}

View File

@ -0,0 +1,25 @@
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';
describe('LeaveRequests (e2e)', () => {
let app: INestApplication;
const BASE = '/leave-requests';
beforeAll(async () => { app = await createApp(); });
beforeEach(async () => {
// await resetDb(app);
});
afterAll(async () => {
const prisma = app.get(PrismaService);
await app.close();
await prisma.$disconnect();
});
it(`GET ${BASE} → 200`, async () => {
const res = await request(app.getHttpServer()).get(BASE);
expect(res.status).toBe(200);
});
});

View File

@ -0,0 +1,26 @@
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';
describe('PayPeriods (e2e)', () => {
let app: INestApplication;
const BASE = '/pay-periods';
beforeAll(async () => { app = await createApp(); });
beforeEach(async () => {
// await resetDb(app);
});
afterAll(async () => {
const prisma = app.get(PrismaService);
await app.close();
await prisma.$disconnect();
});
it(`GET ${BASE} → 200`, async () => {
const res = await request(app.getHttpServer()).get(BASE);
expect(res.status).toBe(200);
//ajouter ici GET /pay-periods/date/:date etc.
});
});

25
test/shifts.e2e-spec.ts Normal file
View File

@ -0,0 +1,25 @@
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';
describe('Shifts (e2e)', () => {
let app: INestApplication;
const BASE = '/shifts';
beforeAll(async () => { app = await createApp(); });
beforeEach(async () => {
// await resetDb(app);
});
afterAll(async () => {
const prisma = app.get(PrismaService);
await app.close();
await prisma.$disconnect();
});
it(`GET ${BASE} → 200`, async () => {
const res = await request(app.getHttpServer()).get(BASE);
expect(res.status).toBe(200);
});
});

View File

@ -0,0 +1,25 @@
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';
describe('Timesheets (e2e)', () => {
let app: INestApplication;
const BASE = '/timesheets';
beforeAll(async () => { app = await createApp(); });
beforeEach(async () => {
// await resetDb(app);
});
afterAll(async () => {
const prisma = app.get(PrismaService);
await app.close();
await prisma.$disconnect();
});
it(`GET ${BASE} → 200`, async () => {
const res = await request(app.getHttpServer()).get(BASE);
expect(res.status).toBe(200);
});
});

21
test/utils/reset-db.ts Normal file
View File

@ -0,0 +1,21 @@
import { INestApplication } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
// export async function resetDb(app: INestApplication) {
// const prisma = app.get(PrismaService);
// const KEEP_USERS = process.env.E2E_KEEP_USERS === '1';
// const excludes = ['_prisma_migrations', ...(KEEP_USERS ? ['users'] : [])];
// const notIn = excludes.map(n => `'${n}'`).join(', ');
// const rows = await prisma.$queryRawUnsafe<Array<{ tablename: string }>>(`
// SELECT table_name AS tablename
// FROM information_schema.tables
// WHERE table_schema = 'public'
// AND table_type = 'BASE TABLE'
// AND table_name NOT IN (${notIn})
// `);
// if (!rows.length) return;
// const list = rows.map(r => `"public"."${r.tablename}"`).join(', ');
// await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${list} RESTART IDENTITY CASCADE;`);
// }

20
test/utils/testing-app.ts Normal file
View File

@ -0,0 +1,20 @@
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { AppModule } from 'src/app.module';
// si tu overrides des guards, garde-les comme avant
export async function createApp(): Promise<INestApplication> {
const mod = await Test.createTestingModule({ imports: [AppModule] }).compile();
const app = mod.createNestApplication();
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
transform: true,
transformOptions: { enableImplicitConversion: true },
forbidNonWhitelisted: true,
validateCustomDecorators: true,
}));
await app.init();
return app;
}

0
tsx Normal file
View File