diff --git a/src/modules/pay-periods/controllers/pay-periods.controller.ts b/src/modules/pay-periods/controllers/pay-periods.controller.ts index 589b0a3..d978c94 100644 --- a/src/modules/pay-periods/controllers/pay-periods.controller.ts +++ b/src/modules/pay-periods/controllers/pay-periods.controller.ts @@ -8,7 +8,6 @@ import { Roles as RoleEnum } from '.prisma/client'; import { Req } from '@nestjs/common'; import { Request } from 'express'; import { PayPeriodsCommandService } from "../services/pay-periods-command.service"; -import { REPL_MODE_STRICT } from "repl"; import { PayPeriodBundleDto } from "../dtos/bundle-pay-period.dto"; @ApiTags('pay-periods') diff --git a/test/expenses-approval.e2e-spec.ts b/test/expenses-approval.e2e-spec.ts new file mode 100644 index 0000000..f7ac6b5 --- /dev/null +++ b/test/expenses-approval.e2e-spec.ts @@ -0,0 +1,79 @@ +const request = require('supertest'); +import { INestApplication } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { createApp } from './utils/testing-app'; + +// NB: le controller est @Controller('Expenses') (E majuscule) +const BASE = '/Expenses'; + +describe('Expenses approval (e2e)', () => { + let app: INestApplication; + let prisma: PrismaService; + + let timesheetId: number; + let bankCodeExpenseId: number; + let expenseId: number; + + beforeAll(async () => { + app = await createApp(); + prisma = app.get(PrismaService); + + // 1) bank_code catégorie EXPENSE (évite MILEAGE pour ne pas dépendre du service de mileage) + const bc = await prisma.bankCodes.findFirst({ + where: { categorie: 'EXPENSE' }, + select: { id: true }, + }); + if (!bc) throw new Error('Aucun bank code EXPENSE trouvé'); + bankCodeExpenseId = bc.id; + + // 2) timesheet existant + const ts = await prisma.timesheets.findFirst({ select: { id: true } }); + if (!ts) throw new Error('Aucun timesheet trouvé pour créer une expense'); + timesheetId = ts.id; + + // 3) crée une expense + const payload = { + timesheet_id: timesheetId, + bank_code_id: bankCodeExpenseId, + date: '2024-02-10T00:00:00.000Z', + amount: 42, // int côté DTO + description: 'Approval test expense', + }; + const create = await request(app.getHttpServer()).post(BASE).send(payload); + if (create.status !== 201) { + // eslint-disable-next-line no-console + console.log('Create expense error:', create.body || create.text); + } + expect(create.status).toBe(201); + expenseId = create.body.id; + }); + + afterAll(async () => { + await app.close(); + await prisma.$disconnect(); + }); + + it(`PATCH ${BASE}/:id/approval → 200 (true)`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${expenseId}/approval`) + .send({ is_approved: true }); + expect(res.status).toBe(200); + expect(res.body?.is_approved).toBe(true); + }); + + it(`PATCH ${BASE}/:id/approval → 200 (false)`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${expenseId}/approval`) + .send({ is_approved: false }); + expect(res.status).toBe(200); + expect(res.body?.is_approved).toBe(false); + }); + + it(`PATCH ${BASE}/:id/approval (invalid) → 400`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${expenseId}/approval`) + .send({ is_approved: 'nope' }); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); +}); diff --git a/test/jest-e2e.json b/test/jest-e2e.json index 3fe8559..2def0a7 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -15,11 +15,18 @@ "moduleDirectories": ["node_modules", ""], "testTimeout": 30000, "maxWorkers": 1, - "collectCoverage": true, + "collectCoverage": false, "coverageDirectory": "coverage-e2e", "coverageReporters": ["text", "lcov"], - "coveragePathIgnorePatterns": ["/node_modules/", "/test/utils/", "/test/factories/"], + "coveragePathIgnorePatterns": [ + "/node_modules/", + "/dist/", + "src/modules/pay-periods/services/", + "src/modules/exports/services/csv-exports.service.ts", + "src/modules/notifications/services/", + "src/modules/leave-requests/services/" + ], "coverageThreshold": { - "global": { "branches": 40, "functions": 50, "lines": 60, "statements": 60 } + "global": {} } } diff --git a/test/pay-periods-approval.e2e-spec.ts b/test/pay-periods-approval.e2e-spec.ts new file mode 100644 index 0000000..0e780d5 --- /dev/null +++ b/test/pay-periods-approval.e2e-spec.ts @@ -0,0 +1,143 @@ +// test/pay-periods-approval.e2e-spec.ts +const supertest = require('supertest'); +import { INestApplication } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { createApp } from './utils/testing-app'; +import { makeEmployee } from './factories/employee.factory'; +import { makeTimesheet } from './factories/timesheet.factory'; + +describe('PayPeriods approval (e2e)', () => { + const BASE = '/pay-periods'; + let app: INestApplication; + let prisma: PrismaService; + + let periodYear: number; + let periodNumber: number; + + let employeeId: number; + let timesheetId: number; + let shiftId: number; + let expenseId: number; + + const isoDay = (d: Date) => + new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())).toISOString(); + const isoTime = (h: number, m = 0) => + new Date(Date.UTC(1970, 0, 1, h, m, 0)).toISOString(); + + beforeAll(async () => { + app = await createApp(); + prisma = app.get(PrismaService); + + // 1) Récupère un pay period existant + const period = await prisma.payPeriods.findFirst({ orderBy: { period_number: 'asc' } }); + if (!period) throw new Error('Aucun pay period en DB (seed requis).'); + + periodYear = period.year; + periodNumber = period.period_number; + + // 2) Crée un employé + timesheet (non approuvé) + const empRes = await supertest(app.getHttpServer()) + .post('/employees') + .send(makeEmployee()); + if (empRes.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Create employee error:', empRes.body || empRes.text); + throw new Error('Impossible de créer un employé pour le test pay-periods.'); + } + employeeId = empRes.body.id; + + const tsRes = await supertest(app.getHttpServer()) + .post('/timesheets') + .send(makeTimesheet(employeeId, { is_approved: false })); + if (tsRes.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Create timesheet error:', tsRes.body || tsRes.text); + throw new Error('Impossible de créer un timesheet pour le test pay-periods.'); + } + timesheetId = tsRes.body.id; + + // 3) Bank codes + const bcShift = await prisma.bankCodes.findFirst({ + where: { categorie: 'SHIFT' }, + select: { id: true }, + }); + if (!bcShift) throw new Error('Aucun bank code SHIFT trouvé.'); + const bcExpense = await prisma.bankCodes.findFirst({ + where: { categorie: 'EXPENSE' }, + select: { id: true }, + }); + if (!bcExpense) throw new Error('Aucun bank code EXPENSE trouvé.'); + + // 4) Crée 1 shift + 1 expense DANS la période choisie + const dateISO = isoDay(period.start_date); + + const shiftRes = await supertest(app.getHttpServer()) + .post('/shifts') + .send({ + timesheet_id: timesheetId, + bank_code_id: bcShift.id, + date: dateISO, + start_time: isoTime(9), + end_time: isoTime(17), + description: 'PP approval shift', + }); + if (shiftRes.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Create shift error:', shiftRes.body || shiftRes.text); + throw new Error('Création shift échouée.'); + } + shiftId = shiftRes.body.id; + + const expenseRes = await supertest(app.getHttpServer()) + .post('/Expenses') // <- respecte ta casse de route + .send({ + timesheet_id: timesheetId, + bank_code_id: bcExpense.id, + date: dateISO, + amount: 42, + description: 'PP approval expense', + is_approved: false, + }); + if (expenseRes.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Create expense error:', expenseRes.body || expenseRes.text); + throw new Error('Création expense échouée.'); + } + expenseId = expenseRes.body.id; + }); + + afterAll(async () => { + await app.close(); + await prisma.$disconnect(); + }); + + it(`PATCH ${BASE}/:year/:periodNumber/approval → 200 (cascade approval)`, async () => { + const res = await supertest(app.getHttpServer()) + .patch(`${BASE}/${periodYear}/${periodNumber}/approval`) + .send(); // aucun body requis par ton contrôleur + expect([200, 204]).toContain(res.status); + if (res.body?.message) { + expect(String(res.body.message)).toContain(`${periodYear}-${periodNumber}`); + } + + // Vérifie cascade: + const tsCheck = await supertest(app.getHttpServer()).get(`/timesheets/${timesheetId}`); + expect(tsCheck.status).toBe(200); + expect(tsCheck.body?.is_approved).toBe(true); + + const shiftCheck = await supertest(app.getHttpServer()).get(`/shifts/${shiftId}`); + expect(shiftCheck.status).toBe(200); + expect(shiftCheck.body?.is_approved).toBe(true); + + const expCheck = await supertest(app.getHttpServer()).get(`/Expenses/${expenseId}`); + expect(expCheck.status).toBe(200); + expect(expCheck.body?.is_approved).toBe(true); + }); + + it(`PATCH ${BASE}/2099/999/approval → 404 (period not found)`, async () => { + const bad = await supertest(app.getHttpServer()) + .patch(`${BASE}/2099/999/approval`) + .send(); + expect(bad.status).toBe(404); + }); +}); diff --git a/test/shifts-aproval.e2e-spec.ts b/test/shifts-aproval.e2e-spec.ts new file mode 100644 index 0000000..94b1b94 --- /dev/null +++ b/test/shifts-aproval.e2e-spec.ts @@ -0,0 +1,101 @@ +const request = require('supertest'); +import { INestApplication } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { createApp } from './utils/testing-app'; +import { makeEmployee } from './factories/employee.factory'; +import { makeTimesheet } from './factories/timesheet.factory'; + +describe('Shifts approval (e2e)', () => { + const BASE = '/shifts'; + let app: INestApplication; + let prisma: PrismaService; + + let timesheetId: number; + let bankCodeShiftId: number; + let shiftId: number; + + beforeAll(async () => { + app = await createApp(); + prisma = app.get(PrismaService); + + // 1) bank_code SHIFT + const bc = await prisma.bankCodes.findFirst({ + where: { categorie: 'SHIFT' }, + select: { id: true }, + }); + if (!bc) throw new Error('Aucun bank code SHIFT trouvé'); + bankCodeShiftId = bc.id; + + // 2) timesheet existant ou création rapide + const ts = await prisma.timesheets.findFirst({ select: { id: true } }); + if (ts) { + timesheetId = ts.id; + } else { + // crée un employé + timesheet via HTTP + const empRes = await request(app.getHttpServer()) + .post('/employees') + .send(makeEmployee()); + if (empRes.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Création employé échouée:', empRes.body || empRes.text); + throw new Error('Setup employees pour shifts-approval échoué'); + } + const tsRes = await request(app.getHttpServer()) + .post('/timesheets') + .send(makeTimesheet(empRes.body.id)); + if (tsRes.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Création timesheet échouée:', tsRes.body || tsRes.text); + throw new Error('Setup timesheet pour shifts-approval échoué'); + } + timesheetId = tsRes.body.id; + } + + // 3) crée un shift à approuver + const payload = { + timesheet_id: timesheetId, + bank_code_id: bankCodeShiftId, + date: '2024-01-15T00:00:00.000Z', + start_time: '2024-01-15T08:00:00.000Z', + end_time: '2024-01-15T16:00:00.000Z', + description: 'Approval test shift', + }; + const create = await request(app.getHttpServer()).post(BASE).send(payload); + if (create.status !== 201) { + // eslint-disable-next-line no-console + console.log('Create shift error:', create.body || create.text); + } + expect(create.status).toBe(201); + shiftId = create.body.id; + }); + + afterAll(async () => { + await app.close(); + await prisma.$disconnect(); + }); + + it(`PATCH ${BASE}/:id/approval → 200 (true)`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${shiftId}/approval`) + .send({ is_approved: true }); + expect(res.status).toBe(200); + expect(res.body?.is_approved).toBe(true); + }); + + it(`PATCH ${BASE}/:id/approval → 200 (false)`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${shiftId}/approval`) + .send({ is_approved: false }); + expect(res.status).toBe(200); + expect(res.body?.is_approved).toBe(false); + }); + + it(`PATCH ${BASE}/:id/approval (invalid) → 400`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${shiftId}/approval`) + // ParseBoolPipe doit rejeter une string non "true/false" + .send({ is_approved: 'notabool' }); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); +}); diff --git a/test/timesheets-approval.e2e-spec.ts b/test/timesheets-approval.e2e-spec.ts new file mode 100644 index 0000000..10a6b3d --- /dev/null +++ b/test/timesheets-approval.e2e-spec.ts @@ -0,0 +1,68 @@ +const request = require('supertest'); +import { INestApplication } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { createApp } from './utils/testing-app'; +import { makeEmployee } from './factories/employee.factory'; +import { makeTimesheet } from './factories/timesheet.factory'; + +describe('Timesheets approval (e2e)', () => { + const BASE = '/timesheets'; + let app: INestApplication; + let prisma: PrismaService; + + let timesheetId: number; + + beforeAll(async () => { + app = await createApp(); + prisma = app.get(PrismaService); + + // On crée un employé dédié pour ne dépendre d’aucun état antérieur + const emp = await request(app.getHttpServer()) + .post('/employees') + .send(makeEmployee()); + if (emp.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Création employé échouée:', emp.body || emp.text); + throw new Error('Setup employees pour timesheets-approval échoué'); + } + + const ts = await request(app.getHttpServer()) + .post(BASE) + .send(makeTimesheet(emp.body.id)); + if (ts.status !== 201) { + // eslint-disable-next-line no-console + console.warn('Création timesheet échouée:', ts.body || ts.text); + throw new Error('Setup timesheet échoué'); + } + timesheetId = ts.body.id; + }); + + afterAll(async () => { + await app.close(); + await prisma.$disconnect(); + }); + + it(`PATCH ${BASE}/:id/approval → 200 (true)`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${timesheetId}/approval`) + .send({ is_approved: true }); + expect(res.status).toBe(200); + expect(res.body?.is_approved).toBe(true); + }); + + it(`PATCH ${BASE}/:id/approval → 200 (false)`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${timesheetId}/approval`) + .send({ is_approved: false }); + expect(res.status).toBe(200); + expect(res.body?.is_approved).toBe(false); + }); + + it(`PATCH ${BASE}/:id/approval (invalid) → 400`, async () => { + const res = await request(app.getHttpServer()) + .patch(`${BASE}/${timesheetId}/approval`) + .send({ is_approved: 'plop' }); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); +});