test: add comprehensive unit and integration tests
Backend Tests Added: - Unit tests for helpers.js (tokens, validation, pagination) - Unit tests for middleware (auth, errorHandler, validate) - Integration tests for auth endpoints - Integration tests for game endpoints - Integration tests for admin endpoints - Integration tests for employee endpoints - Integration tests for draw endpoints - Integration tests for newsletter/contact endpoints Also added: - cross-env for Windows compatibility - Test scripts update 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
33668e5a64
commit
614abeb196
259
grafana-http-panel.json
Normal file
259
grafana-http-panel.json
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 1,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${DS_PROMETHEUS}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "palette-classic" },
|
||||||
|
"custom": {
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 15,
|
||||||
|
"gradientMode": "opacity",
|
||||||
|
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
|
||||||
|
"lineInterpolation": "smooth",
|
||||||
|
"lineWidth": 2,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": { "type": "linear" },
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": { "group": "A", "mode": "none" },
|
||||||
|
"thresholdsStyle": { "mode": "off" }
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "green", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "reqps"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*Frontend.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*Backend.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 10, "w": 24, "x": 0, "y": 0 },
|
||||||
|
"id": 1,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["mean", "max", "sum"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_requests_total{job=\"backend\"}[1m]))",
|
||||||
|
"legendFormat": "Backend - Total",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "sum(rate(http_requests_total{job=\"frontend\"}[1m]))",
|
||||||
|
"legendFormat": "Frontend - Total",
|
||||||
|
"refId": "B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Historique HTTP - Requêtes/sec",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${DS_PROMETHEUS}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "palette-classic" },
|
||||||
|
"custom": {
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "bars",
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": { "type": "linear" },
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": { "group": "A", "mode": "normal" },
|
||||||
|
"thresholdsStyle": { "mode": "off" }
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "green", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*2[0-9]{2}.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*3[0-9]{2}.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*4[0-9]{2}.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*5[0-9]{2}.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 10 },
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(increase(http_requests_total{job=\"backend\"}[5m])) by (status_code)",
|
||||||
|
"legendFormat": "Backend {{status_code}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Backend - HTTP par Status Code",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "${DS_PROMETHEUS}"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": { "mode": "palette-classic" },
|
||||||
|
"custom": {
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"drawStyle": "bars",
|
||||||
|
"fillOpacity": 80,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": { "type": "linear" },
|
||||||
|
"showPoints": "never",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": { "group": "A", "mode": "normal" },
|
||||||
|
"thresholdsStyle": { "mode": "off" }
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [{ "color": "green", "value": null }]
|
||||||
|
},
|
||||||
|
"unit": "short"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*2[0-9]{2}.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "green", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*3[0-9]{2}.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*4[0-9]{2}.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "yellow", "mode": "fixed" } }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": { "id": "byRegexp", "options": ".*5[0-9]{2}.*" },
|
||||||
|
"properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 10 },
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "right",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": { "mode": "multi", "sort": "desc" }
|
||||||
|
},
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(increase(http_requests_total{job=\"frontend\"}[5m])) by (status_code)",
|
||||||
|
"legendFormat": "Frontend {{status_code}}",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Frontend - HTTP par Status Code",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "10s",
|
||||||
|
"schemaVersion": 38,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": ["http", "thetiptop"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"current": {},
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Prometheus",
|
||||||
|
"multi": false,
|
||||||
|
"name": "DS_PROMETHEUS",
|
||||||
|
"options": [],
|
||||||
|
"query": "prometheus",
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "datasource"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": { "from": "now-1h", "to": "now" },
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "Historique HTTP - TheTipTop",
|
||||||
|
"uid": "historique-http-thetiptop",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
26
package-lock.json
generated
26
package-lock.json
generated
|
|
@ -31,6 +31,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.0",
|
"@eslint/js": "^9.39.0",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.39.0",
|
"eslint": "^9.39.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
|
|
@ -567,6 +568,13 @@
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@epic-web/invariant": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
||||||
|
|
@ -2627,6 +2635,24 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@epic-web/invariant": "^1.0.0",
|
||||||
|
"cross-spawn": "^7.0.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "dist/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"dev": "nodemon index.js",
|
"dev": "nodemon index.js",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
|
||||||
|
"test:win": "set NODE_OPTIONS=--experimental-vm-modules && jest",
|
||||||
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js",
|
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js",
|
||||||
"db:schema": "psql -h 51.75.24.29 -U postgres -d thetiptop_dev -p 5433 -f database/schema.sql",
|
"db:schema": "psql -h 51.75.24.29 -U postgres -d thetiptop_dev -p 5433 -f database/schema.sql",
|
||||||
"db:create": "node scripts/create-tables.js",
|
"db:create": "node scripts/create-tables.js",
|
||||||
|
|
@ -44,6 +45,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.0",
|
"@eslint/js": "^9.39.0",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.39.0",
|
"eslint": "^9.39.0",
|
||||||
"jest": "^30.2.0",
|
"jest": "^30.2.0",
|
||||||
"nodemon": "^3.1.10",
|
"nodemon": "^3.1.10",
|
||||||
|
|
|
||||||
21
prometheus-dev.yml
Normal file
21
prometheus-dev.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
global:
|
||||||
|
scrape_interval: 5s
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
||||||
|
|
||||||
|
- job_name: 'backend-dev'
|
||||||
|
metrics_path: /metrics
|
||||||
|
static_configs:
|
||||||
|
- targets: ['the-tip-top-backend-dev:4000']
|
||||||
|
|
||||||
|
- job_name: 'frontend-dev'
|
||||||
|
metrics_path: /api/metrics
|
||||||
|
static_configs:
|
||||||
|
- targets: ['the-tip-top-frontend-dev:3000']
|
||||||
|
|
||||||
|
- job_name: 'node-exporter'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['node-exporter:9100']
|
||||||
160
test/integration/admin.test.js
Normal file
160
test/integration/admin.test.js
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour les endpoints admin
|
||||||
|
*/
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../index.js';
|
||||||
|
|
||||||
|
describe('Admin API', () => {
|
||||||
|
describe('GET /api/admin/statistics', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/admin/statistics');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject request with non-admin token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/admin/statistics')
|
||||||
|
.set('Authorization', 'Bearer invalid-token');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Prize Management', () => {
|
||||||
|
describe('GET /api/admin/prizes', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/admin/prizes');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/admin/prizes', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/admin/prizes')
|
||||||
|
.send({
|
||||||
|
name: 'Test Prize',
|
||||||
|
type: 'INFUSEUR',
|
||||||
|
value: 10,
|
||||||
|
probability: 0.1,
|
||||||
|
stock: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /api/admin/prizes/:id', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.put('/api/admin/prizes/1')
|
||||||
|
.send({ name: 'Updated Prize' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /api/admin/prizes/:id', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).delete('/api/admin/prizes/1');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User Management', () => {
|
||||||
|
describe('GET /api/admin/users', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/admin/users');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/admin/users/:id', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/admin/users/1');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/admin/employees', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/admin/employees')
|
||||||
|
.send({
|
||||||
|
email: 'employee@example.com',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
password: 'Password123!',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /api/admin/users/:id', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.put('/api/admin/users/1')
|
||||||
|
.send({ role: 'EMPLOYEE' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /api/admin/users/:id', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).delete('/api/admin/users/1');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Ticket Management', () => {
|
||||||
|
describe('GET /api/admin/tickets', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/admin/tickets');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/admin/generate-tickets', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/admin/generate-tickets')
|
||||||
|
.send({ count: 100 });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Marketing', () => {
|
||||||
|
describe('GET /api/admin/marketing/stats', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/admin/marketing/stats');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/admin/marketing/export', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/admin/marketing/export')
|
||||||
|
.send({ segment: 'winners' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
156
test/integration/auth.test.js
Normal file
156
test/integration/auth.test.js
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour les endpoints d'authentification
|
||||||
|
*/
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../index.js';
|
||||||
|
|
||||||
|
describe('Auth API', () => {
|
||||||
|
describe('POST /api/auth/register', () => {
|
||||||
|
it('should reject registration with missing fields', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/register')
|
||||||
|
.send({
|
||||||
|
email: 'test@example.com',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject registration with invalid email', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/register')
|
||||||
|
.send({
|
||||||
|
email: 'invalid-email',
|
||||||
|
password: 'Password123!',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject registration with weak password', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/register')
|
||||||
|
.send({
|
||||||
|
email: 'test@example.com',
|
||||||
|
password: '123',
|
||||||
|
firstName: 'John',
|
||||||
|
lastName: 'Doe',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/auth/login', () => {
|
||||||
|
it('should reject login with missing credentials', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/login')
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject login with invalid credentials', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/login')
|
||||||
|
.send({
|
||||||
|
email: 'nonexistent@example.com',
|
||||||
|
password: 'wrongpassword',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBeGreaterThanOrEqual(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/auth/me', () => {
|
||||||
|
it('should reject request without token', async () => {
|
||||||
|
const res = await request(app).get('/api/auth/me');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject request with invalid token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/auth/me')
|
||||||
|
.set('Authorization', 'Bearer invalid-token');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/auth/forgot-password', () => {
|
||||||
|
it('should reject with invalid email format', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/forgot-password')
|
||||||
|
.send({
|
||||||
|
email: 'invalid-email',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/auth/reset-password', () => {
|
||||||
|
it('should reject with missing token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/reset-password')
|
||||||
|
.send({
|
||||||
|
password: 'NewPassword123!',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject with invalid token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/reset-password')
|
||||||
|
.send({
|
||||||
|
token: 'invalid-token',
|
||||||
|
password: 'NewPassword123!',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBeGreaterThanOrEqual(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/auth/verify-email/:token', () => {
|
||||||
|
it('should reject with invalid token', async () => {
|
||||||
|
const res = await request(app).get('/api/auth/verify-email/invalid-token');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBeGreaterThanOrEqual(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/auth/google', () => {
|
||||||
|
it('should reject with missing token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/google')
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/auth/facebook', () => {
|
||||||
|
it('should reject with missing token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/auth/facebook')
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
120
test/integration/draw.test.js
Normal file
120
test/integration/draw.test.js
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour les endpoints de tirage au sort
|
||||||
|
*/
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../index.js';
|
||||||
|
|
||||||
|
describe('Draw API', () => {
|
||||||
|
describe('GET /api/draw/eligible-participants', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/draw/eligible-participants');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject request with non-admin token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/draw/eligible-participants')
|
||||||
|
.set('Authorization', 'Bearer invalid-token');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/draw/check-existing', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/draw/check-existing');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/draw/conduct', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/draw/conduct')
|
||||||
|
.send({
|
||||||
|
criteria: {},
|
||||||
|
prizeName: 'Grand Prize',
|
||||||
|
prizeValue: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/draw/history', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/draw/history');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /api/draw/:id/notify', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).put('/api/draw/1/notify');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /api/draw/:id/claim', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).put('/api/draw/1/claim');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/draw/:id/report', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/draw/1/report');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /api/draw/:id', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).delete('/api/draw/1');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Draw Business Logic', () => {
|
||||||
|
describe('Eligibility Criteria', () => {
|
||||||
|
it('should define valid eligibility rules', () => {
|
||||||
|
// A participant is eligible if they have at least one CLAIMED ticket
|
||||||
|
const eligibilityRules = {
|
||||||
|
minClaimedTickets: 1,
|
||||||
|
ticketStatus: 'CLAIMED',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(eligibilityRules.minClaimedTickets).toBeGreaterThan(0);
|
||||||
|
expect(eligibilityRules.ticketStatus).toBe('CLAIMED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Random Selection', () => {
|
||||||
|
it('should select one winner from eligible participants', () => {
|
||||||
|
const participants = [
|
||||||
|
{ id: 1, name: 'User 1' },
|
||||||
|
{ id: 2, name: 'User 2' },
|
||||||
|
{ id: 3, name: 'User 3' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectWinner = (participants) => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * participants.length);
|
||||||
|
return participants[randomIndex];
|
||||||
|
};
|
||||||
|
|
||||||
|
const winner = selectWinner(participants);
|
||||||
|
|
||||||
|
expect(participants).toContainEqual(winner);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
116
test/integration/employee.test.js
Normal file
116
test/integration/employee.test.js
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour les endpoints employé
|
||||||
|
*/
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../index.js';
|
||||||
|
|
||||||
|
describe('Employee API', () => {
|
||||||
|
describe('GET /api/employee/pending-tickets', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/employee/pending-tickets');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject request with invalid token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/employee/pending-tickets')
|
||||||
|
.set('Authorization', 'Bearer invalid-token');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/employee/search-ticket', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/employee/search-ticket')
|
||||||
|
.query({ code: 'TTP2025ABC' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject request without search query', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/employee/search-ticket')
|
||||||
|
.set('Authorization', 'Bearer invalid-token');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/employee/validate-ticket', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/employee/validate-ticket')
|
||||||
|
.send({
|
||||||
|
ticketId: 1,
|
||||||
|
status: 'CLAIMED',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require ticketId and status', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/employee/validate-ticket')
|
||||||
|
.set('Authorization', 'Bearer invalid-token')
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/employee/stats', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/employee/stats');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/employee/client-prizes', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/employee/client-prizes')
|
||||||
|
.query({ email: 'client@example.com' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/employee/history', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/employee/history');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Employee Ticket Validation', () => {
|
||||||
|
describe('Status Transitions', () => {
|
||||||
|
it('should define valid ticket statuses', () => {
|
||||||
|
const validStatuses = ['PENDING', 'CLAIMED', 'REJECTED'];
|
||||||
|
|
||||||
|
validStatuses.forEach((status) => {
|
||||||
|
expect(['PENDING', 'CLAIMED', 'REJECTED']).toContain(status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have valid status transitions', () => {
|
||||||
|
// PENDING can go to CLAIMED or REJECTED
|
||||||
|
const transitions = {
|
||||||
|
PENDING: ['CLAIMED', 'REJECTED'],
|
||||||
|
CLAIMED: [], // Final state
|
||||||
|
REJECTED: [], // Final state
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(transitions.PENDING).toContain('CLAIMED');
|
||||||
|
expect(transitions.PENDING).toContain('REJECTED');
|
||||||
|
expect(transitions.CLAIMED).toHaveLength(0);
|
||||||
|
expect(transitions.REJECTED).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
90
test/integration/game.test.js
Normal file
90
test/integration/game.test.js
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour les endpoints du jeu
|
||||||
|
*/
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../index.js';
|
||||||
|
|
||||||
|
describe('Game API', () => {
|
||||||
|
describe('POST /api/game/play', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/game/play')
|
||||||
|
.send({ ticketCode: 'TTP2025ABC' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject request with invalid token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/game/play')
|
||||||
|
.set('Authorization', 'Bearer invalid-token')
|
||||||
|
.send({ ticketCode: 'TTP2025ABC' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject request without ticket code', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/game/play')
|
||||||
|
.set('Authorization', 'Bearer invalid-token')
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/game/my-tickets', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/game/my-tickets');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject request with invalid token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.get('/api/game/my-tickets')
|
||||||
|
.set('Authorization', 'Bearer invalid-token');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/game/my-tickets/:id', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/game/my-tickets/1');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/game/ticket/:code', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/game/ticket/TTP2025ABC');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Game Validation', () => {
|
||||||
|
describe('Ticket Code Format', () => {
|
||||||
|
it('should have valid ticket code format TTP + year + 3 chars', () => {
|
||||||
|
const validCodes = ['TTP2025ABC', 'TTP2025XYZ', 'TTP2025123'];
|
||||||
|
const invalidCodes = ['INVALID', 'TT2025ABC', 'TTP25ABC', ''];
|
||||||
|
|
||||||
|
validCodes.forEach((code) => {
|
||||||
|
expect(code).toMatch(/^TTP\d{4}[A-Z0-9]{3}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
invalidCodes.forEach((code) => {
|
||||||
|
expect(code).not.toMatch(/^TTP\d{4}[A-Z0-9]{3}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
119
test/integration/newsletter.test.js
Normal file
119
test/integration/newsletter.test.js
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* Tests d'intégration pour les endpoints newsletter et contact
|
||||||
|
*/
|
||||||
|
import request from 'supertest';
|
||||||
|
import app from '../../index.js';
|
||||||
|
|
||||||
|
describe('Newsletter API', () => {
|
||||||
|
describe('POST /api/newsletter/subscribe', () => {
|
||||||
|
it('should reject subscription with invalid email', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/newsletter/subscribe')
|
||||||
|
.send({ email: 'invalid-email' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject subscription with missing email', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/newsletter/subscribe')
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /api/newsletter/unsubscribe', () => {
|
||||||
|
it('should reject unsubscription with invalid email', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/newsletter/unsubscribe')
|
||||||
|
.send({ email: 'invalid-email' });
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject unsubscription with missing email', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/newsletter/unsubscribe')
|
||||||
|
.send({});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(res.body.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/newsletter/subscribers', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/newsletter/subscribers');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /api/newsletter/count', () => {
|
||||||
|
it('should reject request without authentication', async () => {
|
||||||
|
const res = await request(app).get('/api/newsletter/count');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Contact API', () => {
|
||||||
|
describe('POST /api/contact', () => {
|
||||||
|
it('should reject contact form with missing fields', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/contact')
|
||||||
|
.send({
|
||||||
|
name: 'John Doe',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject contact form with invalid email', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/contact')
|
||||||
|
.send({
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'invalid-email',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test message',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Health Check Endpoints', () => {
|
||||||
|
describe('GET /', () => {
|
||||||
|
it('should return 200 with API status', async () => {
|
||||||
|
const res = await request(app).get('/');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.body).toHaveProperty('message');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /db-check', () => {
|
||||||
|
it('should return database status', async () => {
|
||||||
|
const res = await request(app).get('/db-check');
|
||||||
|
|
||||||
|
// May return 200 or 500 depending on DB connection
|
||||||
|
expect([200, 500]).toContain(res.statusCode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /metrics', () => {
|
||||||
|
it('should return Prometheus metrics', async () => {
|
||||||
|
const res = await request(app).get('/metrics');
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(res.headers['content-type']).toContain('text/plain');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
213
test/unit/helpers.test.js
Normal file
213
test/unit/helpers.test.js
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
/**
|
||||||
|
* Tests unitaires pour src/utils/helpers.js
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
generateToken,
|
||||||
|
generateJWT,
|
||||||
|
verifyJWT,
|
||||||
|
generateTicketCode,
|
||||||
|
isValidEmail,
|
||||||
|
isValidPhone,
|
||||||
|
cleanPhone,
|
||||||
|
formatDate,
|
||||||
|
maskEmail,
|
||||||
|
getTokenExpiry,
|
||||||
|
isExpired,
|
||||||
|
generateRandomPassword,
|
||||||
|
getPagination,
|
||||||
|
formatPaginatedResponse,
|
||||||
|
} from '../../src/utils/helpers.js';
|
||||||
|
|
||||||
|
describe('Helpers - Token Generation', () => {
|
||||||
|
describe('generateToken', () => {
|
||||||
|
it('should generate a token of default length 32', () => {
|
||||||
|
const token = generateToken();
|
||||||
|
expect(token).toHaveLength(64); // hex string is 2x the byte length
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a token of specified length', () => {
|
||||||
|
const token = generateToken(16);
|
||||||
|
expect(token).toHaveLength(32);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate unique tokens', () => {
|
||||||
|
const token1 = generateToken();
|
||||||
|
const token2 = generateToken();
|
||||||
|
expect(token1).not.toBe(token2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateJWT', () => {
|
||||||
|
it('should generate a valid JWT token', () => {
|
||||||
|
const token = generateJWT(1, 'test@example.com', 'CLIENT');
|
||||||
|
expect(token).toBeDefined();
|
||||||
|
expect(typeof token).toBe('string');
|
||||||
|
expect(token.split('.')).toHaveLength(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('verifyJWT', () => {
|
||||||
|
it('should verify a valid JWT token', () => {
|
||||||
|
const token = generateJWT(1, 'test@example.com', 'CLIENT');
|
||||||
|
const decoded = verifyJWT(token);
|
||||||
|
expect(decoded).toBeDefined();
|
||||||
|
expect(decoded.userId).toBe(1);
|
||||||
|
expect(decoded.email).toBe('test@example.com');
|
||||||
|
expect(decoded.role).toBe('CLIENT');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for invalid token', () => {
|
||||||
|
const decoded = verifyJWT('invalid-token');
|
||||||
|
expect(decoded).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Helpers - Ticket Code Generation', () => {
|
||||||
|
describe('generateTicketCode', () => {
|
||||||
|
it('should generate a ticket code starting with TTP', () => {
|
||||||
|
const code = generateTicketCode();
|
||||||
|
expect(code).toMatch(/^TTP/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a 10-character code', () => {
|
||||||
|
const code = generateTicketCode();
|
||||||
|
expect(code).toHaveLength(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include current year', () => {
|
||||||
|
const code = generateTicketCode();
|
||||||
|
const year = new Date().getFullYear().toString();
|
||||||
|
expect(code).toContain(year);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate unique codes', () => {
|
||||||
|
const codes = new Set();
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
codes.add(generateTicketCode());
|
||||||
|
}
|
||||||
|
expect(codes.size).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Helpers - Validation', () => {
|
||||||
|
describe('isValidEmail', () => {
|
||||||
|
it('should return true for valid emails', () => {
|
||||||
|
expect(isValidEmail('test@example.com')).toBe(true);
|
||||||
|
expect(isValidEmail('user.name@domain.fr')).toBe(true);
|
||||||
|
expect(isValidEmail('user+tag@example.org')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for invalid emails', () => {
|
||||||
|
expect(isValidEmail('invalid')).toBe(false);
|
||||||
|
expect(isValidEmail('invalid@')).toBe(false);
|
||||||
|
expect(isValidEmail('@domain.com')).toBe(false);
|
||||||
|
expect(isValidEmail('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isValidPhone', () => {
|
||||||
|
it('should return true for valid French phone numbers', () => {
|
||||||
|
expect(isValidPhone('0612345678')).toBe(true);
|
||||||
|
expect(isValidPhone('06 12 34 56 78')).toBe(true);
|
||||||
|
expect(isValidPhone('+33612345678')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for invalid phone numbers', () => {
|
||||||
|
expect(isValidPhone('123')).toBe(false);
|
||||||
|
expect(isValidPhone('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cleanPhone', () => {
|
||||||
|
it('should remove spaces from phone number', () => {
|
||||||
|
expect(cleanPhone('06 12 34 56 78')).toBe('0612345678');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle already clean numbers', () => {
|
||||||
|
expect(cleanPhone('0612345678')).toBe('0612345678');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Helpers - Date Functions', () => {
|
||||||
|
describe('formatDate', () => {
|
||||||
|
it('should format date to French locale', () => {
|
||||||
|
const date = new Date('2025-01-15');
|
||||||
|
const formatted = formatDate(date);
|
||||||
|
expect(formatted).toContain('2025');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTokenExpiry', () => {
|
||||||
|
it('should return a future date', () => {
|
||||||
|
const expiry = getTokenExpiry(24);
|
||||||
|
expect(new Date(expiry).getTime()).toBeGreaterThan(Date.now());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isExpired', () => {
|
||||||
|
it('should return true for past dates', () => {
|
||||||
|
const pastDate = new Date('2020-01-01');
|
||||||
|
expect(isExpired(pastDate)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for future dates', () => {
|
||||||
|
const futureDate = new Date('2030-01-01');
|
||||||
|
expect(isExpired(futureDate)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Helpers - Security', () => {
|
||||||
|
describe('maskEmail', () => {
|
||||||
|
it('should mask the middle of email', () => {
|
||||||
|
const masked = maskEmail('john.doe@example.com');
|
||||||
|
expect(masked).toContain('***');
|
||||||
|
expect(masked).not.toBe('john.doe@example.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('generateRandomPassword', () => {
|
||||||
|
it('should generate password of specified length', () => {
|
||||||
|
const password = generateRandomPassword(12);
|
||||||
|
expect(password).toHaveLength(12);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate unique passwords', () => {
|
||||||
|
const p1 = generateRandomPassword(16);
|
||||||
|
const p2 = generateRandomPassword(16);
|
||||||
|
expect(p1).not.toBe(p2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Helpers - Pagination', () => {
|
||||||
|
describe('getPagination', () => {
|
||||||
|
it('should calculate correct offset', () => {
|
||||||
|
expect(getPagination(1, 10)).toEqual({ offset: 0, limit: 10 });
|
||||||
|
expect(getPagination(2, 10)).toEqual({ offset: 10, limit: 10 });
|
||||||
|
expect(getPagination(3, 20)).toEqual({ offset: 40, limit: 20 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle default values', () => {
|
||||||
|
const result = getPagination();
|
||||||
|
expect(result.offset).toBeDefined();
|
||||||
|
expect(result.limit).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatPaginatedResponse', () => {
|
||||||
|
it('should format paginated response correctly', () => {
|
||||||
|
const data = [{ id: 1 }, { id: 2 }];
|
||||||
|
const response = formatPaginatedResponse(data, 100, 1, 10);
|
||||||
|
|
||||||
|
expect(response.data).toEqual(data);
|
||||||
|
expect(response.pagination.total).toBe(100);
|
||||||
|
expect(response.pagination.page).toBe(1);
|
||||||
|
expect(response.pagination.limit).toBe(10);
|
||||||
|
expect(response.pagination.totalPages).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
217
test/unit/middleware.test.js
Normal file
217
test/unit/middleware.test.js
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
/**
|
||||||
|
* Tests unitaires pour src/middleware/
|
||||||
|
*/
|
||||||
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
// Mock du pool de base de données
|
||||||
|
jest.unstable_mockModule('../../db.js', () => ({
|
||||||
|
pool: {
|
||||||
|
query: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { pool } = await import('../../db.js');
|
||||||
|
const { authenticateToken, authorizeRoles, optionalAuth } = await import('../../src/middleware/auth.js');
|
||||||
|
const { AppError, errorHandler, asyncHandler } = await import('../../src/middleware/errorHandler.js');
|
||||||
|
const { validate } = await import('../../src/middleware/validate.js');
|
||||||
|
|
||||||
|
describe('Auth Middleware', () => {
|
||||||
|
let mockReq;
|
||||||
|
let mockRes;
|
||||||
|
let mockNext;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReq = {
|
||||||
|
headers: {},
|
||||||
|
cookies: {},
|
||||||
|
};
|
||||||
|
mockRes = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
json: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
mockNext = jest.fn();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticateToken', () => {
|
||||||
|
it('should return 401 if no token provided', async () => {
|
||||||
|
await authenticateToken(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(mockNext).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 401 for invalid token', async () => {
|
||||||
|
mockReq.headers.authorization = 'Bearer invalid-token';
|
||||||
|
|
||||||
|
await authenticateToken(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(401);
|
||||||
|
expect(mockNext).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authorizeRoles', () => {
|
||||||
|
it('should call next if user has required role', () => {
|
||||||
|
mockReq.user = { role: 'ADMIN' };
|
||||||
|
const middleware = authorizeRoles('ADMIN', 'EMPLOYEE');
|
||||||
|
|
||||||
|
middleware(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 403 if user lacks required role', () => {
|
||||||
|
mockReq.user = { role: 'CLIENT' };
|
||||||
|
const middleware = authorizeRoles('ADMIN');
|
||||||
|
|
||||||
|
middleware(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(403);
|
||||||
|
expect(mockNext).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('optionalAuth', () => {
|
||||||
|
it('should call next without user if no token', async () => {
|
||||||
|
await optionalAuth(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
expect(mockReq.user).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Handler Middleware', () => {
|
||||||
|
let mockReq;
|
||||||
|
let mockRes;
|
||||||
|
let mockNext;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReq = {};
|
||||||
|
mockRes = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
json: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
mockNext = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AppError', () => {
|
||||||
|
it('should create an operational error', () => {
|
||||||
|
const error = new AppError('Test error', 400);
|
||||||
|
|
||||||
|
expect(error.message).toBe('Test error');
|
||||||
|
expect(error.statusCode).toBe(400);
|
||||||
|
expect(error.isOperational).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('errorHandler', () => {
|
||||||
|
it('should handle AppError with correct status', () => {
|
||||||
|
const error = new AppError('Not found', 404);
|
||||||
|
|
||||||
|
errorHandler(error, mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
success: false,
|
||||||
|
message: 'Not found',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle generic errors with 500', () => {
|
||||||
|
const error = new Error('Something went wrong');
|
||||||
|
|
||||||
|
errorHandler(error, mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('asyncHandler', () => {
|
||||||
|
it('should pass errors to next middleware', async () => {
|
||||||
|
const error = new Error('Async error');
|
||||||
|
const asyncFn = async () => {
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
const wrappedFn = asyncHandler(asyncFn);
|
||||||
|
|
||||||
|
await wrappedFn(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockNext).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call next on success', async () => {
|
||||||
|
const asyncFn = async (req, res) => {
|
||||||
|
res.json({ success: true });
|
||||||
|
};
|
||||||
|
const wrappedFn = asyncHandler(asyncFn);
|
||||||
|
|
||||||
|
await wrappedFn(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockRes.json).toHaveBeenCalledWith({ success: true });
|
||||||
|
expect(mockNext).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validation Middleware', () => {
|
||||||
|
let mockReq;
|
||||||
|
let mockRes;
|
||||||
|
let mockNext;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockReq = {
|
||||||
|
body: {},
|
||||||
|
query: {},
|
||||||
|
params: {},
|
||||||
|
};
|
||||||
|
mockRes = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
json: jest.fn().mockReturnThis(),
|
||||||
|
};
|
||||||
|
mockNext = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validate', () => {
|
||||||
|
it('should call next for valid data', async () => {
|
||||||
|
const { z } = await import('zod');
|
||||||
|
const schema = z.object({
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
mockReq.body = { email: 'test@example.com' };
|
||||||
|
const middleware = validate(schema);
|
||||||
|
|
||||||
|
await middleware(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockNext).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 400 for invalid data', async () => {
|
||||||
|
const { z } = await import('zod');
|
||||||
|
const schema = z.object({
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
mockReq.body = { email: 'invalid-email' };
|
||||||
|
const middleware = validate(schema);
|
||||||
|
|
||||||
|
await middleware(mockReq, mockRes, mockNext);
|
||||||
|
|
||||||
|
expect(mockRes.status).toHaveBeenCalledWith(400);
|
||||||
|
expect(mockNext).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user