@mlagie/sql-connector 2.0.5 → 2.0.7
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.
- package/.github/workflows/publish.yml +35 -25
- package/README.md +107 -14
- package/docs/fr/README.md +114 -19
- package/package.json +8 -3
- package/releases/2.0.6.md +80 -0
- package/releases/2.0.7.md +38 -0
- package/socket.yml +4 -0
- package/src/models/Model.js +35 -33
- package/src/models/ModelInstance.js +19 -28
- package/src/utils/{buildQuery.js → buildQuery/buildQuery.js} +6 -6
- package/src/utils/buildQuery/count.js +11 -0
- package/src/utils/formatObject.js +1 -0
- package/src/utils/generateCondition.js +0 -8
- package/tests/Model.test.js +666 -0
- package/tests/ModelInstance.test.js +335 -0
- package/tests/buildQuery.test.js +134 -19
- package/tests/connect.test.js +14 -4
- package/tests/formatObject.test.js +9 -0
- package/tests/generateCondition.test.js +200 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
const { ModelInstance } = require("../src/models/ModelInstance");
|
|
2
|
+
// 1. Importez le vrai gestionnaire de connexion de votre ORM
|
|
3
|
+
const connexionManager = require("../src/db/connexion");
|
|
4
|
+
const util = require("util");
|
|
5
|
+
// 2. Définissez votre structure de mock locale
|
|
6
|
+
const mockExecute = jest.fn();
|
|
7
|
+
const mockPool = {
|
|
8
|
+
promise: () => ({
|
|
9
|
+
execute: mockExecute
|
|
10
|
+
})
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Empêche les logs d'erreurs volontaires des tests de polluer le terminal
|
|
14
|
+
jest.mock('@mlagie/logger', () => ({
|
|
15
|
+
error: jest.fn(),
|
|
16
|
+
logs: jest.fn(),
|
|
17
|
+
serveur: jest.fn()
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe("ModelInstance Unit Tests - v2.0.6", () => {
|
|
21
|
+
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
// 3. 🔥 LE FIX MAGIQUE : On force l'ORM à utiliser le mock localement
|
|
24
|
+
// Cela écrase la connexion réelle par votre pool simulé sans dépendre du comportement de jest.mock()
|
|
25
|
+
connexionManager.setConnexion(mockPool);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
jest.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("updateOne() doit exécuter une requête UPDATE valide", async () => {
|
|
33
|
+
const mockData = { id: 1, name: "callisto-report" };
|
|
34
|
+
const mockSchema = { schemaDict: { id: { primary_key: true } } };
|
|
35
|
+
const instance = new ModelInstance("ProjectPipeline", mockData, mockSchema);
|
|
36
|
+
|
|
37
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
38
|
+
|
|
39
|
+
// Appel de la méthode
|
|
40
|
+
await instance.updateOne({ name: "nouveau-nom" });
|
|
41
|
+
|
|
42
|
+
expect(mockExecute).toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("Hydratation - _getTargetRow doit gérer un tableau de données imbriqué", () => {
|
|
46
|
+
// Lignes 47, 52 : Couvre le cas où rows est un tableau de lignes
|
|
47
|
+
const instance = new ModelInstance("ProjectPipeline", [[{ id: 99, name: "nested" }]]);
|
|
48
|
+
expect(instance.id).toBe(99);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("updateOne() - Doit lever une erreur et la logger en cas d'échec SQL", async () => {
|
|
52
|
+
// Lignes 129-130 : Bloc .catch de updateOne
|
|
53
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
54
|
+
mockExecute.mockRejectedValue(new Error("MySQL Down"));
|
|
55
|
+
|
|
56
|
+
await expect(instance.updateOne({ name: "fail" })).rejects.toThrow("MySQL Down");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("delete() - Doit exécuter une suppression groupée et gérer le succès/échec", async () => {
|
|
60
|
+
// Lignes 138 - 154 : Méthode delete(filter) globale
|
|
61
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
62
|
+
|
|
63
|
+
// Cas 1 : Succès avec lignes affectées
|
|
64
|
+
mockExecute.mockResolvedValue([{ affectedRows: 2 }]);
|
|
65
|
+
let result = await instance.delete({ status: "old" });
|
|
66
|
+
expect(result).toBe(1);
|
|
67
|
+
|
|
68
|
+
// Cas 2 : Aucune ligne affectée
|
|
69
|
+
mockExecute.mockResolvedValue([{ affectedRows: 0 }]);
|
|
70
|
+
result = await instance.delete({ status: "old" });
|
|
71
|
+
expect(result).toBe(0);
|
|
72
|
+
|
|
73
|
+
// Cas 3 : Échec SQL (Couvre le bloc .catch de delete)
|
|
74
|
+
mockExecute.mockRejectedValue(new Error("Delete Failed"));
|
|
75
|
+
await expect(instance.delete({ status: "old" })).rejects.toThrow("Delete Failed");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("deleteOne() - Doit lever une erreur en cas d'échec SQL", async () => {
|
|
79
|
+
// Lignes 173-176 : Bloc .catch de deleteOne
|
|
80
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
81
|
+
mockExecute.mockRejectedValue(new Error("MySQL Fatal Delete"));
|
|
82
|
+
|
|
83
|
+
await expect(instance.deleteOne()).rejects.toThrow("MySQL Fatal Delete");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("customRequest() - Doit exécuter du SQL brut et hydrater ou renvoyer 0", async () => {
|
|
87
|
+
// Lignes 185-195 : Méthode customRequest
|
|
88
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
89
|
+
|
|
90
|
+
// Cas 1 : La requête renvoie des données
|
|
91
|
+
mockExecute.mockResolvedValue([[{ id: 5, custom_field: "value" }]]);
|
|
92
|
+
const resData = await instance.customRequest("SELECT * FROM optimization");
|
|
93
|
+
expect(resData).toBeDefined();
|
|
94
|
+
|
|
95
|
+
// Cas 2 : La requête renvoie un tableau vide (Ligne 192 : rows[0].length == 0)
|
|
96
|
+
mockExecute.mockResolvedValue([[]]);
|
|
97
|
+
const resEmpty = await instance.customRequest("SELECT * FROM empty");
|
|
98
|
+
expect(resEmpty).toBe(0);
|
|
99
|
+
|
|
100
|
+
// Cas 3 : Échec de la requête (Couvre le bloc .catch de customRequest)
|
|
101
|
+
mockExecute.mockRejectedValue(new Error("Query Bad Syntax"));
|
|
102
|
+
await expect(instance.customRequest("SELECT BAD")).rejects.toThrow("Query Bad Syntax");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// === COUVRE LIGNES 47, 52 (Setters & Échec JSON.parse) ===
|
|
106
|
+
test("Hydratation - Doit exécuter les setters dynamiques et capter les faux JSON", () => {
|
|
107
|
+
const mockData = { id: 1, config: "{malformed json" };
|
|
108
|
+
const instance = new ModelInstance("ProjectPipeline", mockData);
|
|
109
|
+
|
|
110
|
+
// Ligne 52 : Déclenche le setter dynamique (setSafe)
|
|
111
|
+
instance.id = 45;
|
|
112
|
+
expect(instance.id).toBe(45);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// === COUVRE LIGNES 75-79 (JSON parsing dans getRecordData) ===
|
|
116
|
+
test("getRecordData - Doit analyser les chaînes JSON ou renvoyer le tableau brut", () => {
|
|
117
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
118
|
+
|
|
119
|
+
// On simule une structure où _data est un tableau contenant une chaîne JSON
|
|
120
|
+
instance._data = ["[{\"id\": 10, \"name\": \"json-extracted\"}]"];
|
|
121
|
+
|
|
122
|
+
const record = instance.getRecordData();
|
|
123
|
+
expect(record).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// === COUVRE LIGNES 99-102 (Extraction de Clé Primaire avec succès) ===
|
|
127
|
+
test("updateOne() - Doit extraire la clé primaire depuis le dictionnaire de schéma", async () => {
|
|
128
|
+
const mockData = { id: 77, name: "pipeline-target" };
|
|
129
|
+
const mockSchema = { schemaDict: { id: { primary_key: true } } };
|
|
130
|
+
const instance = new ModelInstance("ProjectPipeline", mockData, mockSchema);
|
|
131
|
+
|
|
132
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
133
|
+
|
|
134
|
+
// Lignes 99-102 : Utilise 'id' comme critère WHERE unique
|
|
135
|
+
const affected = await instance.updateOne({ name: "pipeline-updated" });
|
|
136
|
+
expect(affected).toBe(1);
|
|
137
|
+
|
|
138
|
+
const sqlGenerated = mockExecute.mock.calls[0][0];
|
|
139
|
+
expect(sqlGenerated).toContain("UPDATE ProjectPipeline SET `name` = 'pipeline-updated' WHERE `id` = 77");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// === COUVRE LIGNES 120-124 (Bloc catch de repli de updateOne) ===
|
|
143
|
+
test("updateOne() - Doit basculer sur le fallback d'évaluation si le parsing crash", async () => {
|
|
144
|
+
const mockData = { id: 1, name: "pipeline-fallback-test" };
|
|
145
|
+
const instance = new ModelInstance("ProjectPipeline", mockData);
|
|
146
|
+
|
|
147
|
+
// 🔥 LE FIX : On passe un dictionnaire de schéma structurellement valide (un objet)
|
|
148
|
+
// mais totalement vide. L'évaluation des clés primaires échouera proprement,
|
|
149
|
+
// ce qui forcera l'activation du bloc catch de repli (fallback) de updateOne
|
|
150
|
+
// sans provoquer de panne sur Reflect.get !
|
|
151
|
+
instance._schema = { schemaDict: {} };
|
|
152
|
+
|
|
153
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
154
|
+
|
|
155
|
+
// Exécution de la méthode
|
|
156
|
+
const affected = await instance.updateOne({ status: "patched" });
|
|
157
|
+
expect(affected).toBe(1);
|
|
158
|
+
|
|
159
|
+
const sqlGenerated = mockExecute.mock.calls[0][0];
|
|
160
|
+
// On valide que le traitement s'est terminé avec succès
|
|
161
|
+
expect(sqlGenerated).toContain("UPDATE ProjectPipeline SET `status` = 'patched' WHERE `id` = 1 AND `name` = 'pipeline-fallback-test'");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// === COUVRE LIGNE 138 (delete avec affectedRows > 0) ===
|
|
165
|
+
test("delete() - Doit renvoyer 1 si des éléments ont été affectés par la suppression", async () => {
|
|
166
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
167
|
+
|
|
168
|
+
// Ligne 138 (Cas affecté)
|
|
169
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
170
|
+
const result = await instance.delete({ status: "archived" });
|
|
171
|
+
expect(result).toBe(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// === COUVRE LIGNE 178 (deleteOne avec affectedRows > 0) ===
|
|
175
|
+
test("deleteOne() - Doit renvoyer 1 si l'élément courant a été supprimé", async () => {
|
|
176
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 200 });
|
|
177
|
+
|
|
178
|
+
// Ligne 178 (Cas affecté)
|
|
179
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
180
|
+
const result = await instance.deleteOne();
|
|
181
|
+
expect(result).toBe(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// === COUVRE LES BLOCS D'ERREURS DÉJÀ EXISTANTS ===
|
|
185
|
+
test("updateOne() - Doit lever une erreur et la logger en cas d'échec SQL", async () => {
|
|
186
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
187
|
+
mockExecute.mockRejectedValue(new Error("MySQL Down"));
|
|
188
|
+
|
|
189
|
+
await expect(instance.updateOne({ name: "fail" })).rejects.toThrow("MySQL Down");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("delete() - Doit propager l'erreur en cas de rejet SQL", async () => {
|
|
193
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
194
|
+
mockExecute.mockRejectedValue(new Error("Delete Failed"));
|
|
195
|
+
|
|
196
|
+
await expect(instance.delete({ status: "old" })).rejects.toThrow("Delete Failed");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("deleteOne() - Doit lever une erreur en cas d'échec SQL", async () => {
|
|
200
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
201
|
+
mockExecute.mockRejectedValue(new Error("MySQL Fatal Delete"));
|
|
202
|
+
|
|
203
|
+
await expect(instance.deleteOne()).rejects.toThrow("MySQL Fatal Delete");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("customRequest() - Doit exécuter du SQL brut et hydrater ou renvoyer 0", async () => {
|
|
207
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
208
|
+
|
|
209
|
+
mockExecute.mockResolvedValue([[{ id: 5, custom_field: "value" }]]);
|
|
210
|
+
const resData = await instance.customRequest("SELECT * FROM optimization");
|
|
211
|
+
expect(resData).toBeDefined();
|
|
212
|
+
|
|
213
|
+
mockExecute.mockResolvedValue([[]]);
|
|
214
|
+
const resEmpty = await instance.customRequest("SELECT * FROM empty");
|
|
215
|
+
expect(resEmpty).toBe(0);
|
|
216
|
+
|
|
217
|
+
mockExecute.mockRejectedValue(new Error("Query Bad Syntax"));
|
|
218
|
+
await expect(instance.customRequest("SELECT BAD")).rejects.toThrow("Query Bad Syntax");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("Getter dynamique - retourne la valeur brute si JSON.parse échoue", () => {
|
|
222
|
+
const instance = new ModelInstance("ProjectPipeline", {
|
|
223
|
+
config: "{invalid-json}"
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(instance.config).toBe("{invalid-json}");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("updateOne() met à jour les données locales après succès", async () => {
|
|
230
|
+
const instance = new ModelInstance(
|
|
231
|
+
"ProjectPipeline",
|
|
232
|
+
{ id: 1, name: "before" }
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
236
|
+
|
|
237
|
+
await instance.updateOne({ name: "after" });
|
|
238
|
+
|
|
239
|
+
expect(instance._data.name).toBe("after");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("updateOne() met à jour le premier élément d'un tableau", async () => {
|
|
243
|
+
const instance = new ModelInstance(
|
|
244
|
+
"ProjectPipeline",
|
|
245
|
+
[{ id: 1, name: "before" }]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
249
|
+
|
|
250
|
+
await instance.updateOne({ name: "after" });
|
|
251
|
+
|
|
252
|
+
expect(instance._data[0].name).toBe("after");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("delete() retourne 0 quand aucune ligne n'est supprimée", async () => {
|
|
256
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
257
|
+
|
|
258
|
+
mockExecute.mockResolvedValue([{ affectedRows: 0 }]);
|
|
259
|
+
|
|
260
|
+
expect(await instance.delete({ id: 1 })).toBe(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("delete() retourne 1 quand une ligne est supprimée", async () => {
|
|
264
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
265
|
+
|
|
266
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
267
|
+
|
|
268
|
+
expect(await instance.delete({ id: 1 })).toBe(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("customRequest() retourne les données hydratées", async () => {
|
|
272
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1 });
|
|
273
|
+
|
|
274
|
+
mockExecute.mockResolvedValue([
|
|
275
|
+
[{ id: 123, name: "test" }]
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
const result = await instance.customRequest("SELECT * FROM test");
|
|
279
|
+
|
|
280
|
+
expect(result).toEqual([{ id: 123, name: "test" }]);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("toJSON() retourne les données locales", () => {
|
|
284
|
+
const instance = new ModelInstance("ProjectPipeline", { id: 1, name: "test" });
|
|
285
|
+
|
|
286
|
+
expect(instance.toJSON()).toEqual({ id: 1, name: "test" });
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("updateOne parse une chaîne JSON", async () => {
|
|
290
|
+
const instance = new ModelInstance(
|
|
291
|
+
"users",
|
|
292
|
+
'{"id":1,"name":"john"}'
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
296
|
+
|
|
297
|
+
await instance.updateOne({
|
|
298
|
+
name: "updated"
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(mockExecute).toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("updateOne() - Doit basculer sur le bloc catch global et exécuter le fallback", async () => {
|
|
305
|
+
// 1. On instancie normalement un ModelInstance avec un objet JSON valide
|
|
306
|
+
const mockData = { id: 10, status: "active" };
|
|
307
|
+
const instance = new ModelInstance("ProjectPipeline", mockData);
|
|
308
|
+
|
|
309
|
+
// 2. On espionne la méthode getRecordData() de cette instance spécifique
|
|
310
|
+
const spy = jest.spyOn(instance, 'getRecordData');
|
|
311
|
+
|
|
312
|
+
// 3. Premier appel (dans le try principal) : On jette une erreur pour forcer le passage au catch
|
|
313
|
+
// Deuxième appel (dans le bloc catch) : On renvoie l'objet de secours valide mockData
|
|
314
|
+
spy.mockImplementationOnce(() => {
|
|
315
|
+
throw new Error("Forced exception to enter catch block");
|
|
316
|
+
}).mockImplementationOnce(() => {
|
|
317
|
+
return mockData;
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Simule un retour positif de la base de données
|
|
321
|
+
mockExecute.mockResolvedValue([{ affectedRows: 1 }]);
|
|
322
|
+
|
|
323
|
+
// 4. Exécution de la méthode
|
|
324
|
+
const affected = await instance.updateOne({ status: "patched" });
|
|
325
|
+
|
|
326
|
+
expect(affected).toBe(1);
|
|
327
|
+
expect(mockExecute).toHaveBeenCalled();
|
|
328
|
+
|
|
329
|
+
const sqlGenerated = mockExecute.mock.calls[0][0];
|
|
330
|
+
expect(sqlGenerated).toContain("UPDATE ProjectPipeline SET `status` = 'patched' WHERE `id` = 10 AND `status` = 'active'");
|
|
331
|
+
|
|
332
|
+
// 5. On nettoie le spy pour ne pas impacter les autres tests
|
|
333
|
+
spy.mockRestore();
|
|
334
|
+
});
|
|
335
|
+
});
|
package/tests/buildQuery.test.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { buildSelect, buildQueryParts } = require('../src/utils/buildQuery');
|
|
1
|
+
const { buildSelect, buildQueryParts } = require('../src/utils/buildQuery/buildQuery');
|
|
2
2
|
|
|
3
3
|
describe('Utils - buildQuery.js', () => {
|
|
4
4
|
describe('buildSelect', () => {
|
|
@@ -11,12 +11,12 @@ describe('Utils - buildQuery.js', () => {
|
|
|
11
11
|
expect(buildSelect(['id', 'name'])).toBe('`id`,\n`name`');
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
test('Devrait gérer l
|
|
14
|
+
test('Devrait gérer l\'agrégation SUM avec alias optionnel', () => {
|
|
15
15
|
expect(buildSelect([{ sum: 'price' }])).toBe('SUM(`price`) AS `price`');
|
|
16
16
|
expect(buildSelect([{ sum: 'price', as: 'total' }])).toBe('SUM(`price`) AS `total`');
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
test('Devrait gérer DATE_FORMAT et appliquer l
|
|
19
|
+
test('Devrait gérer DATE_FORMAT et appliquer l\'échappement sur la colonne', () => {
|
|
20
20
|
const select = [{ dateFormat: ['createdAt', '%Y-%m'], as: 'month' }];
|
|
21
21
|
expect(buildSelect(select)).toBe("DATE_FORMAT(`createdAt`, '%Y-%m') AS `month`");
|
|
22
22
|
});
|
|
@@ -76,27 +76,142 @@ describe('Utils - buildQuery.js', () => {
|
|
|
76
76
|
expect(() => buildQueryParts({ limit: 'abc' })).toThrow('Invalid LIMIT value');
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
|
-
|
|
79
|
+
describe('buildQuery - Advanced Fields & Aggregations', () => {
|
|
80
|
+
|
|
81
|
+
test('Should safely compile COUNT(*) aggregated calculations with an expression alias', () => {
|
|
82
|
+
const fields = [{ count: '*', as: 'total_user' }];
|
|
83
|
+
expect(buildSelect(fields)).toBe('COUNT(*) AS `total_user`');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('Should safely compile COUNT(column) on structured column identifiers', () => {
|
|
87
|
+
const fields = [{ count: 'id', as: 'unique_ids' }];
|
|
88
|
+
expect(buildSelect(fields)).toBe('COUNT(`id`) AS `unique_ids`');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('Should correctly process multi-column GROUP BY arrays to avoid ONLY_FULL_GROUP_BY validation issues', () => {
|
|
92
|
+
const options = {
|
|
93
|
+
select: ['status', 'email', { count: '*', as: 'total_user' }],
|
|
94
|
+
groupBy: ['status', 'email']
|
|
95
|
+
};
|
|
96
|
+
const parts = buildQueryParts(options);
|
|
80
97
|
|
|
81
|
-
|
|
98
|
+
expect(parts).toContain('GROUP BY `status`, `email`');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("COUNT DISTINCT sur plusieurs colonnes", () => {
|
|
102
|
+
expect(
|
|
103
|
+
buildSelect([
|
|
104
|
+
{
|
|
105
|
+
count: ["truc", "bidule"],
|
|
106
|
+
as: "total"
|
|
107
|
+
}
|
|
108
|
+
])
|
|
109
|
+
).toBe("COUNT(`truc`) + COUNT(`bidule`) AS `total`");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("COUNT conditionnel avec NULL", () => {
|
|
113
|
+
expect(
|
|
114
|
+
buildSelect([
|
|
115
|
+
{
|
|
116
|
+
count: {
|
|
117
|
+
truc: null
|
|
118
|
+
},
|
|
119
|
+
as: "total"
|
|
120
|
+
}
|
|
121
|
+
])
|
|
122
|
+
).toBe("COUNT(CASE WHEN `truc` = NULL THEN 1 END) AS `total`");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("COUNT conditionnel avec NOT NULL", () => {
|
|
126
|
+
expect(
|
|
127
|
+
buildSelect([
|
|
128
|
+
{
|
|
129
|
+
count: {
|
|
130
|
+
truc: "NOT NULL"
|
|
131
|
+
},
|
|
132
|
+
as: "total"
|
|
133
|
+
}
|
|
134
|
+
])
|
|
135
|
+
).toBe("COUNT(CASE WHEN `truc` = 'NOT NULL' THEN 1 END) AS `total`");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("COUNT refuse un nombre", () => {
|
|
139
|
+
expect(() =>
|
|
140
|
+
buildSelect([
|
|
141
|
+
{
|
|
142
|
+
count: 1515
|
|
143
|
+
}
|
|
144
|
+
])
|
|
145
|
+
).toThrow(
|
|
146
|
+
"Invalid field type for COUNT. Must be a string, array, or object."
|
|
147
|
+
);
|
|
148
|
+
});
|
|
82
149
|
|
|
83
|
-
test('Should safely compile COUNT(*) aggregated calculations with an expression alias', () => {
|
|
84
|
-
const fields = [{ count: '*', as: 'total_user' }];
|
|
85
|
-
expect(buildSelect(fields)).toBe('COUNT(*) AS `total_user`');
|
|
86
150
|
});
|
|
87
151
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
152
|
+
describe("buildQuery - buildGroupByItem", () => {
|
|
153
|
+
test('Return directly an error if group is not a string.', () => {
|
|
154
|
+
const options = {
|
|
155
|
+
select: ['status', 'email', { count: '*', as: 'total_user' }],
|
|
156
|
+
groupBy: ['status', 1255]
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
expect(() => buildQueryParts(options)).toThrow(
|
|
160
|
+
"Group by items must be strings"
|
|
161
|
+
);
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('Devrait gérer DISTINCT avec alias', () => {
|
|
166
|
+
const fields = [
|
|
167
|
+
{
|
|
168
|
+
distinct: 'email',
|
|
169
|
+
as: 'unique_email'
|
|
170
|
+
}
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
expect(buildSelect(fields))
|
|
174
|
+
.toBe('DISTINCT `email` AS `unique_email`');
|
|
91
175
|
});
|
|
92
176
|
|
|
93
|
-
test(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
177
|
+
test("GROUP BY simple", () => {
|
|
178
|
+
expect(
|
|
179
|
+
buildQueryParts({
|
|
180
|
+
groupBy: ["status"]
|
|
181
|
+
})
|
|
182
|
+
).toBe("GROUP BY `status`");
|
|
183
|
+
});
|
|
99
184
|
|
|
100
|
-
|
|
185
|
+
test("count simple", () => {
|
|
186
|
+
expect(
|
|
187
|
+
buildSelect([
|
|
188
|
+
{ count: "id" }
|
|
189
|
+
])
|
|
190
|
+
).toBe("COUNT(`id`)");
|
|
101
191
|
});
|
|
102
|
-
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
test("buildSelect supporte *", () => {
|
|
195
|
+
expect(
|
|
196
|
+
buildSelect(["*"])
|
|
197
|
+
).toBe("*");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("SUM sans alias génère un alias automatique", () => {
|
|
201
|
+
expect(
|
|
202
|
+
buildSelect([
|
|
203
|
+
{
|
|
204
|
+
sum: "price"
|
|
205
|
+
}
|
|
206
|
+
])
|
|
207
|
+
).toBe("SUM(`price`) AS `price`");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("LIMIT décimal invalide", () => {
|
|
211
|
+
expect(() =>
|
|
212
|
+
buildQueryParts({
|
|
213
|
+
limit: 10.5
|
|
214
|
+
})
|
|
215
|
+
).toThrow();
|
|
216
|
+
});
|
|
217
|
+
});
|
package/tests/connect.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// 1. CHARGER LE MOCK EN PREMIER
|
|
2
|
-
const { mockPool, mockEnd } = require('./mysqlMock');
|
|
2
|
+
const { mockPool, mockEnd } = require('./mysqlMock');
|
|
3
3
|
|
|
4
4
|
// 2. CHARGER LES MODULES DU PROJET ENSUITE
|
|
5
5
|
const { connect, logout } = require('../src/db/connect'); // Ajustez le chemin vers votre dossier src
|
|
@@ -7,14 +7,14 @@ const { getConnexion } = require('../src/db/connexion');
|
|
|
7
7
|
const mysql = require('mysql2');
|
|
8
8
|
|
|
9
9
|
describe("Tests unitaires avec Mock - connect.js", () => {
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
jest.clearAllMocks();
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
test("connect() devrait créer un pool mysql2 et définir la connexion globale", async () => {
|
|
16
16
|
const config = { host: 'localhost', user: 'root', database: 'test' };
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
await connect(config);
|
|
19
19
|
|
|
20
20
|
// Maintenant mysql.createPool est bien un mock Jest !
|
|
@@ -28,7 +28,17 @@ describe("Tests unitaires avec Mock - connect.js", () => {
|
|
|
28
28
|
setConnexion(mockPool);
|
|
29
29
|
|
|
30
30
|
await logout();
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
expect(mockEnd).toHaveBeenCalled();
|
|
33
33
|
});
|
|
34
|
+
|
|
35
|
+
test("logout() doit gérer une erreur lors de la fermeture", async () => {
|
|
36
|
+
mockPool.end.mockImplementation((callback) => {
|
|
37
|
+
callback(new Error("Close Error"));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await logout();
|
|
41
|
+
|
|
42
|
+
expect(mockPool.end).toHaveBeenCalled();
|
|
43
|
+
});
|
|
34
44
|
});
|
|
@@ -34,4 +34,13 @@ describe('Utils - formatObject.js', () => {
|
|
|
34
34
|
expect(result.age).toBe(25);
|
|
35
35
|
expect(result.isValid).toBe(true);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
test("formatObject couvre hasOwnProperty", () => {
|
|
39
|
+
const parent = { test: 1 };
|
|
40
|
+
const obj = Object.create(parent);
|
|
41
|
+
|
|
42
|
+
obj.name = "john";
|
|
43
|
+
|
|
44
|
+
formatObject(obj);
|
|
45
|
+
});
|
|
37
46
|
});
|