test-tp1-ynov-react-kleas17 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Validation error used by all business validators.
3
+ */
4
+ class ValidationError extends Error {
5
+ /**
6
+ * @param {string} code Stable machine-readable error code.
7
+ * @param {string} message Human-readable error message.
8
+ */
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ }
13
+ }
14
+ const ERROR_MESSAGES = {
15
+ INVALID_DATE: 'Date de naissance invalide',
16
+ UNDERAGE: "L'utilisateur doit avoir au moins 18 ans",
17
+ INVALID_POSTAL_TYPE: 'Le code postal doit être une chaîne de caractères',
18
+ INVALID_POSTAL_CODE: 'Code postal français invalide',
19
+ INVALID_IDENTITY_TYPE: "Le nom ou le prénom doit être une chaîne de caractères",
20
+ XSS_DETECTED: 'Contenu HTML détecté',
21
+ INVALID_NAME: 'Caractères invalides dans le nom',
22
+ INVALID_EMAIL_TYPE: "L'email doit être une chaîne de caractères",
23
+ INVALID_EMAIL: "Format d'email invalide",
24
+ DUPLICATE_EMAIL: 'Cet email est déjà utilisé'
25
+ };
26
+
27
+ /**
28
+ * Computes age in full years from a birth date.
29
+ * @param {Date} birthDate Birth date to evaluate.
30
+ * @returns {number} Age in years.
31
+ */
32
+ function calculateAge(birthDate) {
33
+ const today = new Date();
34
+ let age = today.getFullYear() - birthDate.getFullYear();
35
+ const monthDiff = today.getMonth() - birthDate.getMonth();
36
+ const hasBirthdayPassed = monthDiff > 0 || monthDiff === 0 && today.getDate() >= birthDate.getDate();
37
+ if (!hasBirthdayPassed) {
38
+ age -= 1;
39
+ }
40
+ return age;
41
+ }
42
+
43
+ /**
44
+ * Validates legal age (18+) and date realism.
45
+ * @param {Date} birthDate Birth date to validate.
46
+ * @returns {number} Computed age when valid.
47
+ * @throws {ValidationError} When date is invalid, unrealistic, or user is underage.
48
+ */
49
+ function validateAge(birthDate) {
50
+ if (!(birthDate instanceof Date) || Number.isNaN(birthDate.getTime())) {
51
+ throw new ValidationError('INVALID_DATE', ERROR_MESSAGES.INVALID_DATE);
52
+ }
53
+ const now = new Date();
54
+ if (birthDate > now) {
55
+ throw new ValidationError('INVALID_DATE', ERROR_MESSAGES.INVALID_DATE);
56
+ }
57
+ const age = calculateAge(birthDate);
58
+ if (age > 120) {
59
+ throw new ValidationError('INVALID_DATE', ERROR_MESSAGES.INVALID_DATE);
60
+ }
61
+ if (age < 18) {
62
+ throw new ValidationError('UNDERAGE', ERROR_MESSAGES.UNDERAGE);
63
+ }
64
+ return age;
65
+ }
66
+
67
+ /**
68
+ * Validates French postal code format.
69
+ * @param {string} code Postal code to validate.
70
+ * @throws {ValidationError} When input is not a string or not 5 digits.
71
+ */
72
+ function validatePostalCode(code) {
73
+ if (typeof code !== 'string') {
74
+ throw new ValidationError('INVALID_TYPE', ERROR_MESSAGES.INVALID_POSTAL_TYPE);
75
+ }
76
+ if (!/^\d{5}$/.test(code)) {
77
+ throw new ValidationError('INVALID_POSTAL_CODE', ERROR_MESSAGES.INVALID_POSTAL_CODE);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Validates identity-like fields (name, surname, city).
83
+ * @param {string} value Value to validate.
84
+ * @throws {ValidationError} When input is invalid or contains HTML.
85
+ */
86
+ function validateIdentity(value) {
87
+ if (typeof value !== 'string') {
88
+ throw new ValidationError('INVALID_TYPE', ERROR_MESSAGES.INVALID_IDENTITY_TYPE);
89
+ }
90
+ if (/<[^>]*>/.test(value)) {
91
+ throw new ValidationError('XSS_DETECTED', ERROR_MESSAGES.XSS_DETECTED);
92
+ }
93
+ const nameRegex = /^[A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\- ]+$/;
94
+ if (!nameRegex.test(value)) {
95
+ throw new ValidationError('INVALID_NAME', ERROR_MESSAGES.INVALID_NAME);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Validates email format with strict ASCII rules.
101
+ * @param {string} email Email address to validate.
102
+ * @throws {ValidationError} When input is invalid.
103
+ */
104
+ function validateEmail(email) {
105
+ if (typeof email !== 'string') {
106
+ throw new ValidationError('INVALID_TYPE', ERROR_MESSAGES.INVALID_EMAIL_TYPE);
107
+ }
108
+ const emailRegex = /^[A-Za-z0-9](?:[A-Za-z0-9._%+-]{0,62}[A-Za-z0-9])?@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/;
109
+ if (!emailRegex.test(email) || email.includes('..')) {
110
+ throw new ValidationError('INVALID_EMAIL', ERROR_MESSAGES.INVALID_EMAIL);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Ensures an email does not already exist in the user collection.
116
+ * @param {string} email Email to check.
117
+ * @param {Array<{email:string}>} users Registered users.
118
+ * @throws {ValidationError} When email is already registered.
119
+ */
120
+ function validateUniqueEmail(email, users) {
121
+ const normalizedEmail = email.trim().toLowerCase();
122
+ const found = users.some(user => {
123
+ if (!user || typeof user.email !== 'string') {
124
+ return false;
125
+ }
126
+ return user.email.trim().toLowerCase() === normalizedEmail;
127
+ });
128
+ if (found) {
129
+ throw new ValidationError('DUPLICATE_EMAIL', ERROR_MESSAGES.DUPLICATE_EMAIL);
130
+ }
131
+ }
132
+ export { ValidationError, validateAge, validatePostalCode, validateIdentity, validateEmail, validateUniqueEmail };
@@ -0,0 +1,152 @@
1
+ import {
2
+ ValidationError,
3
+ validateAge,
4
+ validateEmail,
5
+ validateIdentity,
6
+ validatePostalCode,
7
+ validateUniqueEmail,
8
+ } from './validator';
9
+
10
+ describe('ValidationError', () => {
11
+ test('expose le code et le message', () => {
12
+ const error = new ValidationError('TEST_CODE', 'Message test');
13
+ expect(error.code).toBe('TEST_CODE');
14
+ expect(error.message).toBe('Message test');
15
+ });
16
+ });
17
+
18
+ describe('validateAge', () => {
19
+ test('retourne un age numerique pour un adulte', () => {
20
+ const age = validateAge(new Date('1990-01-01'));
21
+ expect(typeof age).toBe('number');
22
+ expect(age).toBeGreaterThanOrEqual(18);
23
+ });
24
+
25
+ test('rejette un type invalide', () => {
26
+ expect(() => validateAge('1990-01-01')).toThrow('Date de naissance invalide');
27
+ });
28
+
29
+ test('rejette une date invalide', () => {
30
+ expect(() => validateAge(new Date('invalid-date'))).toThrow('Date de naissance invalide');
31
+ });
32
+
33
+ test('rejette un utilisateur mineur', () => {
34
+ expect(() => validateAge(new Date('2015-06-01'))).toThrow(
35
+ "L'utilisateur doit avoir au moins 18 ans"
36
+ );
37
+ });
38
+
39
+ test('rejette une date dans le futur', () => {
40
+ expect(() => validateAge(new Date('2999-01-01'))).toThrow('Date de naissance invalide');
41
+ });
42
+
43
+ test('rejette une date trop ancienne (age farfelu)', () => {
44
+ expect(() => validateAge(new Date('0008-08-08'))).toThrow('Date de naissance invalide');
45
+ });
46
+ });
47
+
48
+ describe('validatePostalCode', () => {
49
+ test('accepte un code postal francais valide', () => {
50
+ expect(() => validatePostalCode('75015')).not.toThrow();
51
+ });
52
+
53
+ test('rejette un type non string', () => {
54
+ expect(() => validatePostalCode(75015)).toThrow(
55
+ 'Le code postal doit être une chaîne de caractères'
56
+ );
57
+ });
58
+
59
+ test('rejette un code avec lettres', () => {
60
+ expect(() => validatePostalCode('75A15')).toThrow('Code postal français invalide');
61
+ });
62
+
63
+ test('rejette un code trop court', () => {
64
+ expect(() => validatePostalCode('1234')).toThrow('Code postal français invalide');
65
+ });
66
+
67
+ test('rejette un code trop long', () => {
68
+ expect(() => validatePostalCode('123456')).toThrow('Code postal français invalide');
69
+ });
70
+ });
71
+
72
+ describe('validateIdentity', () => {
73
+ test('accepte nom simple', () => {
74
+ expect(() => validateIdentity('Jean Dupont')).not.toThrow();
75
+ });
76
+
77
+ test('accepte accents et tirets', () => {
78
+ expect(() => validateIdentity('Élodie-Anne')).not.toThrow();
79
+ });
80
+
81
+ test('rejette type non string', () => {
82
+ expect(() => validateIdentity(null)).toThrow(
83
+ 'Le nom ou le prénom doit être une chaîne de caractères'
84
+ );
85
+ });
86
+
87
+ test('rejette contenu HTML', () => {
88
+ expect(() => validateIdentity('<b>Jean</b>')).toThrow('Contenu HTML détecté');
89
+ });
90
+
91
+ test('rejette chiffres et symboles', () => {
92
+ expect(() => validateIdentity('Jean123')).toThrow('Caractères invalides dans le nom');
93
+ expect(() => validateIdentity('Jean!')).toThrow('Caractères invalides dans le nom');
94
+ });
95
+
96
+ test('rejette chaine vide', () => {
97
+ expect(() => validateIdentity('')).toThrow('Caractères invalides dans le nom');
98
+ });
99
+ });
100
+
101
+ describe('validateEmail', () => {
102
+ test('accepte email valide', () => {
103
+ expect(() => validateEmail('jean.dupont@example.com')).not.toThrow();
104
+ });
105
+
106
+ test('rejette type non string', () => {
107
+ expect(() => validateEmail(undefined)).toThrow("L'email doit être une chaîne de caractères");
108
+ });
109
+
110
+ test('rejette format invalide', () => {
111
+ expect(() => validateEmail('jean.dupont')).toThrow("Format d'email invalide");
112
+ });
113
+
114
+ test('rejette email avec espace', () => {
115
+ expect(() => validateEmail('jean dupont@example.com')).toThrow("Format d'email invalide");
116
+ });
117
+
118
+ test('rejette email avec caracteres farfelus (accent, parenthese)', () => {
119
+ expect(() => validateEmail('kleas3.marc@gmaa)l.com')).toThrow("Format d'email invalide");
120
+ expect(() => validateEmail('kleas3.marc@gmaàl.com')).toThrow("Format d'email invalide");
121
+ });
122
+
123
+ test('rejette email avec double point', () => {
124
+ expect(() => validateEmail('jean..dupont@example.com')).toThrow("Format d'email invalide");
125
+ });
126
+ });
127
+
128
+ describe('validateUniqueEmail', () => {
129
+ test('accepte un email non present', () => {
130
+ expect(() =>
131
+ validateUniqueEmail('new@example.com', [{ email: 'jean.dupont@example.com' }])
132
+ ).not.toThrow();
133
+ });
134
+
135
+ test('rejette un email deja present', () => {
136
+ expect(() =>
137
+ validateUniqueEmail('jean.dupont@example.com', [{ email: 'jean.dupont@example.com' }])
138
+ ).toThrow('Cet email est déjà utilisé');
139
+ });
140
+
141
+ test('compare sans tenir compte de la casse', () => {
142
+ expect(() =>
143
+ validateUniqueEmail('JEAN.DUPONT@example.com', [{ email: 'jean.dupont@example.com' }])
144
+ ).toThrow('Cet email est déjà utilisé');
145
+ });
146
+
147
+ test('ignore les entrees invalides dans la liste', () => {
148
+ expect(() =>
149
+ validateUniqueEmail('new@example.com', [null, { email: 42 }, { notEmail: true }])
150
+ ).not.toThrow();
151
+ });
152
+ });
package/package.json ADDED
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "test-tp1-ynov-react-kleas17",
3
+ "version": "1.0.1",
4
+ "private": false,
5
+ "description": "React application package prepared for automated npm publishing.",
6
+ "main": "dist/index.js",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "dependencies": {
14
+ "@testing-library/dom": "^10.4.1",
15
+ "@testing-library/jest-dom": "^6.9.1",
16
+ "@testing-library/react": "^16.3.2",
17
+ "@testing-library/user-event": "^13.5.0",
18
+ "axios": "^1.13.5",
19
+ "react": "^18.2.0",
20
+ "react-dom": "^18.2.0",
21
+ "react-router-dom": "^6.28.0",
22
+ "react-scripts": "5.0.1",
23
+ "web-vitals": "^2.1.4"
24
+ },
25
+ "scripts": {
26
+ "start": "react-scripts start",
27
+ "build": "react-scripts build",
28
+ "build-npm": "node -e \"process.env.NODE_ENV='production'; const fs=require('fs'); fs.rmSync('dist',{ recursive:true, force:true }); fs.mkdirSync('dist',{ recursive:true });\" && node ./node_modules/@babel/cli/bin/babel.js src --out-dir dist --copy-files --ignore \"**/*.test.js\",\"**/*.spec.js\",\"**/__tests__/**\",\"**/__snapshots__/**\"",
29
+ "build-npm:unix": "NODE_ENV=production && rm -rf dist && mkdir dist && npx babel src --out-dir dist --copy-files",
30
+ "build-npm:win": "SET NODE_ENV=production && rm -rf dist && mkdir dist && npx babel src --out-dir dist --copy-files",
31
+ "prepublishOnly": "npm run build-npm",
32
+ "version:major": "npm version major",
33
+ "version:minor": "npm version minor",
34
+ "version:patch": "npm version patch",
35
+ "version:prerelease": "npm version prerelease",
36
+ "version:premajor": "npm version premajor",
37
+ "version:preminor": "npm version preminor",
38
+ "version:prepatch": "npm version prepatch",
39
+ "version:alpha": "npm version prerelease --preid=alpha",
40
+ "version:beta": "npm version prerelease --preid=beta",
41
+ "version:rc": "npm version prerelease --preid=rc",
42
+ "version:patch:message": "npm version patch -m \"Upgrade to %s\"",
43
+ "test": "react-scripts test --coverage --watchAll=false --setupFiles=./.jest/setEnvVars.js",
44
+ "cypress:open": "cypress open",
45
+ "cypress:open:chrome": "cypress open --browser chrome",
46
+ "cypress:open:edge": "cypress open --browser edge",
47
+ "cypress:run": "cypress run",
48
+ "cypress:run:chrome": "cypress run --browser chrome",
49
+ "e2e": "cypress run",
50
+ "docs": "jsdoc -R ../README.md -c jsdoc.config.json -r -d public/docs",
51
+ "eject": "react-scripts eject"
52
+ },
53
+ "eslintConfig": {
54
+ "extends": [
55
+ "react-app",
56
+ "react-app/jest"
57
+ ]
58
+ },
59
+ "browserslist": {
60
+ "production": [
61
+ ">0.2%",
62
+ "not dead",
63
+ "not op_mini all"
64
+ ],
65
+ "development": [
66
+ "last 1 chrome version",
67
+ "last 1 firefox version",
68
+ "last 1 safari version"
69
+ ]
70
+ },
71
+ "jest": {
72
+ "collectCoverageFrom": [
73
+ "src/**/*.{js,jsx}",
74
+ "!src/index.js",
75
+ "!src/reportWebVitals.js",
76
+ "!src/setupTests.js"
77
+ ],
78
+ "coverageThreshold": {
79
+ "global": {
80
+ "branches": 80,
81
+ "functions": 90,
82
+ "lines": 90,
83
+ "statements": 90
84
+ }
85
+ }
86
+ },
87
+ "devDependencies": {
88
+ "@babel/cli": "^7.28.3",
89
+ "@babel/core": "^7.28.5",
90
+ "@babel/plugin-transform-react-jsx": "^7.27.1",
91
+ "cypress": "^13.17.0",
92
+ "jsdoc": "^4.0.4",
93
+ "react-test-renderer": "^18.2.0"
94
+ }
95
+ }