data-primals-engine 1.4.3 → 1.5.0

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.
Files changed (50) hide show
  1. package/README.md +878 -867
  2. package/client/package-lock.json +49 -0
  3. package/client/package.json +1 -0
  4. package/client/src/App.jsx +1 -1
  5. package/client/src/App.scss +13 -3
  6. package/client/src/AssistantChat.scss +3 -2
  7. package/client/src/DashboardView.jsx +569 -569
  8. package/client/src/DataEditor.jsx +2 -2
  9. package/client/src/DataLayout.jsx +1 -8
  10. package/client/src/DataTable.jsx +26 -4
  11. package/client/src/Field.jsx +1825 -1788
  12. package/client/src/FlexDataRenderer.jsx +2 -0
  13. package/client/src/FlexTreeUtils.js +1 -1
  14. package/client/src/KPIDialog.jsx +11 -1
  15. package/client/src/ModelCreator.jsx +1 -2
  16. package/client/src/ModelCreatorField.jsx +23 -27
  17. package/client/src/ModelList.jsx +1 -1
  18. package/client/src/constants.js +1 -1
  19. package/client/src/hooks/useTutorials.jsx +62 -65
  20. package/client/src/translations.js +2 -0
  21. package/package.json +2 -1
  22. package/perf/README.md +147 -0
  23. package/perf/artillery-hooks.js +37 -0
  24. package/perf/perf-shot-hardwork.yml +84 -0
  25. package/perf/perf-shot-search.yml +45 -0
  26. package/perf/setup.yml +26 -0
  27. package/server.js +1 -1
  28. package/src/constants.js +2 -27
  29. package/src/core.js +15 -1
  30. package/src/data.js +1 -1
  31. package/src/defaultModels.js +1544 -1540
  32. package/src/email.js +5 -2
  33. package/src/engine.js +5 -3
  34. package/src/filter.js +5 -3
  35. package/src/modules/assistant/assistant.js +3 -1
  36. package/src/modules/bucket.js +12 -15
  37. package/src/modules/data/data.backup.js +11 -8
  38. package/src/modules/data/data.js +6 -3
  39. package/src/modules/data/data.operations.js +3231 -2999
  40. package/src/modules/data/data.routes.js +1821 -1785
  41. package/src/modules/data/data.scheduling.js +2 -1
  42. package/src/modules/data/data.validation.js +4 -1
  43. package/src/modules/file.js +4 -2
  44. package/src/modules/user.js +4 -1
  45. package/src/modules/workflow.js +9 -10
  46. package/src/openai.jobs.js +2 -0
  47. package/src/packs.js +22 -5
  48. package/src/providers.js +22 -7
  49. package/test/data.integration.test.js +1060 -981
  50. package/test/import_export.integration.test.js +1 -1
@@ -1,982 +1,1061 @@
1
- // __tests__/data.integration.test.js
2
- import { ObjectId } from 'mongodb';
3
- import {vi, expect, describe, it, afterEach,beforeEach, beforeAll, afterAll} from 'vitest';
4
- import { Config } from '../src/config.js';
5
- import {
6
- insertData,
7
- editData,
8
- deleteData,
9
- searchData, installPack, deleteModels, createModel, patchData
10
- } from '../src/index.js';
11
-
12
- import {
13
- modelsCollection as getAppModelsCollection,
14
- getCollection,
15
- getCollectionForUser as getAppUserCollection, getCollectionForUser
16
- } from '../src/modules/mongodb.js';
17
- import {getRandom} from "../src/core.js";
18
- import {generateUniqueName, initEngine} from "../src/setenv.js";
19
- import {purgeData} from "../src/modules/data/data.history.js";
20
-
21
- let testModelsColInstance;
22
- let testDatasColInstance;
23
-
24
- // Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
25
- async function setupTestContext() {
26
-
27
- const currentTestModelName = generateUniqueName('relatedModel');
28
- const currentRelatedModelName = generateUniqueName('comprehensiveModel');
29
-
30
- // Créer un utilisateur unique pour ce test
31
- const currentTestUser = {
32
- username: generateUniqueName('testuserDataIntegration'),
33
- userPlan: 'premium',
34
- email: generateUniqueName('test') + '@example.com'
35
- };
36
-
37
- // Initialize collection instances after the engine is ready
38
- testModelsColInstance = getAppModelsCollection;
39
- testDatasColInstance = await getAppUserCollection(currentTestUser);
40
-
41
- const relatedModelDefinition = {
42
- name: currentRelatedModelName,
43
- _user: currentTestUser.username,
44
- description: 'Model for testing relations',
45
- fields: [
46
- {name: 'relatedName', type: 'string', required: true, unique: true},
47
- {name: 'relatedValue', type: 'number'}
48
- ],
49
- maxRequestData: 10
50
- };
51
-
52
- const comprehensiveTestModelDefinition = {
53
- name: currentTestModelName,
54
- _user: currentTestUser.username,
55
- description: 'A model with all field types and constraints for testing',
56
- fields: [
57
- // String types
58
- {name: 'stringRequired', type: 'string', required: true, hint: 'A required string'},
59
- {name: 'stringUnique', type: 'string', unique: true, hint: 'A unique string'},
60
- {name: 'stringMaxLength', type: 'string', maxlength: 5, hint: 'String with max length 5'},
61
- {name: 'stringDefault', type: 'string', default: 'defaultString'},
62
- {name: 'string_tLang', type: 'string_t', hint: 'Localizable string'},
63
- {name: 'richtextField', type: 'richtext', maxlength: 200},
64
- {name: 'passwordField', type: 'password'},
65
- {name: 'emailField', type: 'email'},
66
- {name: 'phoneField', type: 'phone'},
67
- {name: 'urlField', type: 'url'},
68
- // Number types
69
- {name: 'number', type: 'number'},
70
- {name: 'numberMinMax', type: 'number', min: 10, max: 20},
71
- {name: 'numberStep', type: 'number', step: 0.5},
72
- {name: 'numberDefault', type: 'number', default: 42},
73
- // Boolean
74
- {name: 'booleanField', type: 'boolean'},
75
- {name: 'booleanDefault', type: 'boolean', default: true},
76
- // Date & Datetime
77
- {name: 'dateField', type: 'date'},
78
- {name: 'datetimeField', type: 'datetime', default: 'now'},
79
- {name: 'dateMinMax', type: 'date', min: '2023-01-01', max: '2023-12-31'},
80
- // Enum
81
- {name: 'enumField', type: 'enum', items: ['alpha', 'beta', 'gamma']},
82
- {name: 'enumDefault', type: 'enum', items: ['one', 'two'], default: 'one'},
83
- // Array
84
- {name: 'arrayString', type: 'array', itemsType: 'string', maxItems: 2},
85
- {name: 'arrayNumber', type: 'array', itemsType: 'number', minItems: 1},
86
- {name: 'arrayEnum', type: 'array', itemsType: 'enum', items: ['a', 'b']},
87
- // Relation
88
- {name: 'relationSingle', type: 'relation', relation: currentRelatedModelName},
89
- {name: 'relationMultiple', type: 'relation', relation: currentRelatedModelName, multiple: true},
90
- // File (metadata validation)
91
- {name: 'fileField', type: 'file', mimeTypes: ['image/png', 'application/pdf'], maxSize: 1024 * 10}, // 10KB
92
- // Color
93
- {name: 'colorField', type: 'color'},
94
- // Code
95
- {name: 'codeJsonField', type: 'code', language: 'json'},
96
- {name: 'codeJsField', type: 'code', language: 'javascript', maxlength: 100},
97
- // Object (simple validation, structure not deeply validated by default dataTypes.object)
98
- {name: 'objectField', type: 'object'},
99
- // Model & ModelField (validation of string format, not existence)
100
- {name: 'modelNameField', type: 'model'},
101
- {name: 'modelFieldNameField', type: 'modelField'} // Note: modelField type expects an object {model: 'modelName', field: 'fieldName'}
102
- ],
103
- maxRequestData: 50
104
- };
105
-
106
- if( await testModelsColInstance.find({ $or: [{name: comprehensiveTestModelDefinition.name}, {name: relatedModelDefinition.name}]})) {
107
- // Insérer les modèles en base
108
- await testModelsColInstance.insertMany([
109
- comprehensiveTestModelDefinition,
110
- relatedModelDefinition
111
- ]);
112
- }
113
- await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
114
-
115
- const purge = async ()=>{
116
- await purgeData(currentTestUser);
117
- }
118
-
119
- // Retourner toutes les variables nécessaires pour un test
120
- return {
121
- currentTestUser,
122
- comprehensiveTestModelDefinition,
123
- relatedModelDefinition,
124
- purge
125
- };
126
- }
127
-
128
- let engine;
129
- describe('Intégration des fonctions CRUD de données avec validation complète', () => {
130
-
131
- beforeAll(async () =>{
132
- Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
133
- engine = await initEngine();
134
-
135
- })
136
- describe('insertData avec comprehensiveTestModel', () => {
137
- it('devrait insérer des données valides pour tous les types de champs', async () => {
138
-
139
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
140
- // 1. Créer un suffixe unique pour cette exécution de test
141
- const testSuffix = getRandom(1000, 9999); // Génère un nombre aléatoire
142
-
143
- // 2. Insérer les documents liés avec des noms uniques garantis
144
- const relatedDoc1 = await insertData(relatedModelDefinition.name, {
145
- relatedName: `Rel1_Unique_${testSuffix}`, // Nom unique
146
- relatedValue: 100
147
- }, {}, currentTestUser, false);
148
-
149
- const relatedDoc2 = await insertData(relatedModelDefinition.name, {
150
- relatedName: `Rel2_Unique_${testSuffix}`, // Nom unique
151
- relatedValue: 200
152
- }, {}, currentTestUser, false);
153
-
154
- // S'assurer que les insertions précédentes ont réussi avant de continuer
155
- expect(relatedDoc1.success, "L'insertion du document lié 1 a échoué").toBe(true);
156
- expect(relatedDoc2.success, "L'insertion du document lié 2 a échoué").toBe(true);
157
-
158
- // --- FIN DU SETUP AMÉLIORÉ ---
159
-
160
- const validData = {
161
- stringRequired: 'Valid String',
162
- stringUnique: `UniqueValueForInsert_${testSuffix}`, // Valeur unique garantie
163
- stringMaxLength: '12345',
164
- string_tLang: 'Bonjour',
165
- richtextField: '<p>Valid rich text</p>',
166
- passwordField: 'validPassword123',
167
- emailField: 'valid@example.com',
168
- phoneField: '+1234567890',
169
- urlField: 'https://example.com',
170
- number: 123,
171
- numberMinMax: 15,
172
- numberStep: 10.5,
173
- booleanField: true,
174
- dateField: '2023-07-15',
175
- dateMinMax: '2023-06-01',
176
- enumField: 'alpha',
177
- arrayString: ['one', 'two'],
178
- arrayNumber: [1, 2, 3],
179
- arrayEnum: ['a'],
180
- relationSingle: relatedDoc1.insertedIds[0].toString(),
181
- relationMultiple: [relatedDoc1.insertedIds[0].toString(), relatedDoc2.insertedIds[0].toString()],
182
- // Le mock de fichier est OK, pas besoin de le rendre unique
183
- fileField: { name: 'test.png', type: 'image/png', size: 1024 * 5, guid: `dummy-guid-png-${testSuffix}` },
184
- colorField: '#FF0000',
185
- codeJsonField: { "key": "value" },
186
- codeJsField: 'console.log("hello");',
187
- objectField: { a: 1, b: "test" },
188
- modelNameField: 'someModelName',
189
- modelFieldNameField: { model: 'someModelName', field: 'someFieldName' }
190
- };
191
-
192
- const result = await insertData(comprehensiveTestModelDefinition.name, validData, {}, currentTestUser, false);
193
- expect(result.success, `L'insertion principale a échoué: ${result.error}`).toBe(true);
194
- expect(result.insertedIds).toHaveLength(1);
195
-
196
- console.log(result);
197
- const insertedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(result.insertedIds[0]) });
198
- expect(insertedDoc).not.toBeNull();
199
- expect(insertedDoc.stringRequired).toBe('Valid String');
200
- expect(insertedDoc.stringUnique).toBe(`UniqueValueForInsert_${testSuffix}`); // Vérifier la valeur unique
201
- expect(insertedDoc.stringDefault).toBe('defaultString');
202
- expect(new Date(insertedDoc.datetimeField).getFullYear()).toBe(new Date().getFullYear());
203
- expect(insertedDoc.booleanDefault).toBe(true);
204
- expect(insertedDoc.relationSingle.toString()).toBe(relatedDoc1.insertedIds[0].toString());
205
-
206
- await purge();
207
- }, 10000);
208
-
209
- describe('Validations des champs à l\'insertion', () => {
210
- // --- STRING VALIDATIONS ---
211
- it('devrait rejeter un string requis non fourni', async () => {
212
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
213
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringUnique: 'test' /* stringRequired manquant */ }, {}, currentTestUser, false);
214
- expect(result.success).toBe(false);
215
- expect(result.error).toContain('stringRequired');
216
- await purge();
217
- });
218
-
219
- it('devrait rejeter un string trop long pour maxlength', async () => {
220
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
221
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', stringMaxLength: '123456' }, {}, currentTestUser, false);
222
- expect(result.success).toBe(false);
223
- expect(result.error).toContain('stringMaxLength');
224
- await purge();
225
- });
226
-
227
- it('devrait rejeter une valeur dupliquée pour un champ unique (stringUnique)', async () => {
228
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
229
- await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req1', stringUnique: 'duplicateMe', 'passwordField': 'pass', number: 1, enumField: 'test' }, {}, currentTestUser, false);
230
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req2', stringUnique: 'duplicateMe', 'passwordField': 'pass', number: 1, enumField: 'test' }, {}, currentTestUser, false);
231
- expect(result.success).toBe(false);
232
- await purge();
233
- });
234
-
235
- // --- NUMBER VALIDATIONS ---
236
- it('devrait rejeter un number non numérique', async () => {
237
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
238
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', number: 'not-a-number' }, {}, currentTestUser, false);
239
- expect(result.success).toBe(false);
240
- expect(result.error).toContain('number');
241
- await purge();
242
- });
243
-
244
- it('devrait rejeter un number infur à min (numberMinMax)', async () => {
245
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
246
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', numberMinMax: 5 }, {}, currentTestUser, false);
247
- expect(result.success).toBe(false);
248
- expect(result.error).toContain('numberMinMax');
249
- await purge();
250
- });
251
-
252
- it('devrait rejeter un number supérieur à max (numberMinMax)', async () => {
253
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
254
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', numberMinMax: 25 }, {}, currentTestUser, false);
255
- expect(result.success).toBe(false);
256
- expect(result.error).toContain('numberMinMax');
257
- await purge();
258
- });
259
-
260
- // --- ARRAY VALIDATIONS ---
261
- it('devrait rejeter un array avec trop d\'items (arrayString maxItems: 2)', async () => {
262
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
263
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayString: ['a', 'b', 'c'] }, {}, currentTestUser, false);
264
- expect(result.success).toBe(false);
265
- expect(result.error).toContain('arrayString');
266
- await purge();
267
- });
268
-
269
- it('devrait rejeter un array avec pas assez d\'items (arrayNumber minItems: 1)', async () => {
270
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
271
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayNumber: [] }, {}, currentTestUser, false);
272
- expect(result.success).toBe(false);
273
- expect(result.error).toContain('arrayNumber');
274
- await purge();
275
- });
276
-
277
- it('devrait rejeter un array avec un item de type incorrect (arrayNumber)', async () => {
278
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
279
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayNumber: [1, 'not-a-number', 3] }, {}, currentTestUser, false);
280
- expect(result.success).toBe(false);
281
- expect(result.error).toContain('arrayNumber');
282
- await purge();
283
- });
284
-
285
- // --- FILE VALIDATIONS (metadata) ---
286
- it('devrait rejeter un file avec un mimeType non autorisé', async () => {
287
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
288
- const mockFile = { name: 'test.txt', type: 'text/plain', size: 1024, guid: 'dummy-txt' };
289
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
290
- expect(result.success).toBe(false);
291
- expect(result.error).toContain('image/png'); // Ou un message plus spécifique sur mimeTypes
292
- expect(result.error).toContain('text/plain'); // Ou un message plus spécifique sur mimeTypes
293
- await purge();
294
- });
295
-
296
- it('devrait rejeter un file avec une taille supérieure à maxSize', async () => {
297
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
298
- const mockFile = { name: 'large.png', type: 'image/png', size: 1024 * 15, guid: 'dummy-large' }; // 15KB > 10KB
299
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
300
- expect(result.success).toBe(false);
301
- expect(result.error).toContain('fileField'); // Ou un message plus spécifique sur maxSize
302
- await purge();
303
- });
304
-
305
- // --- EMAIL, URL, PHONE, COLOR (basic format) ---
306
- it('devrait rejeter un email invalide', async () => {
307
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
308
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', emailField: 'not-an-email' }, {}, currentTestUser, false);
309
- expect(result.success).toBe(false);
310
- expect(result.error).toContain('emailField');
311
- await purge();
312
- });
313
- it('devrait rejeter une URL invalide', async () => {
314
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
315
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', urlField: 'not a url' }, {}, currentTestUser, false);
316
- expect(result.success).toBe(false);
317
- expect(result.error).toContain('urlField');
318
- await purge();
319
- });
320
- it('devrait rejeter une couleur invalide', async () => {
321
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
322
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', colorField: '#GGHHII' }, {}, currentTestUser, false);
323
- expect(result.success).toBe(false);
324
- expect(result.error).toContain('colorField');
325
- await purge();
326
- });
327
-
328
- // --- DATE / DATETIME VALIDATIONS ---
329
- it('devrait rejeter une date invalide (format)', async () => {
330
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
331
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', dateField: 'not-a-date' }, {}, currentTestUser, false);
332
- expect(result.success).toBe(false);
333
- expect(result.error).toContain('dateField');
334
- await purge();
335
- });
336
- it('devrait rejeter une date en dehors de min/max (dateMinMax)', async () => {
337
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
338
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', dateMinMax: '2024-01-01' }, {}, currentTestUser, false);
339
- expect(result.success).toBe(false);
340
- expect(result.error).toContain('dateMinMax');
341
- await purge();
342
- });
343
-
344
- // TODO: Ajouter des tests pour les autres types et contraintes (password, richtext, code, object, model, modelField)
345
- // Pour 'code' avec language 'json', tester l'insertion d'un JSON invalide si la validation va jusque-là.
346
- // Pour 'relation', tester l'insertion d'un ObjectId non existant (si la validation vérifie l'existence).
347
- });
348
-
349
- // --- NOUVEAU TEST POUR $find ---
350
- it('devrait insérer des données avec des relations référencées par $find', async () => {
351
- // 1. Insérer les documents liés qui seront trouvés par $find
352
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
353
- const relatedDocA = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeA', relatedValue: 111 }, {}, currentTestUser, false);
354
- const relatedDocB = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeB', relatedValue: 222 }, {}, currentTestUser, false);
355
- const relatedDocC = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeC', relatedValue: 222 }, {}, currentTestUser, false);
356
-
357
- const relatedIdA = relatedDocA.insertedIds[0];
358
- const relatedIdB = relatedDocB.insertedIds[0];
359
- const relatedIdC = relatedDocC.insertedIds[0];
360
-
361
- // 2. Prr les données du document principal utilisant $find
362
- const dataWithFindRelations = {
363
- stringRequired: 'DocWithFind',
364
- stringUnique: 'UniqueFindValue',
365
- passwordField: 'findPass',
366
- enumField: 'beta',
367
- number: 99,
368
- // Utiliser $find pour la relation simple
369
- relationSingle: { $find: { $eq: ["$relatedName", 'FindMeA'] } },
370
- // Utiliser un tableau de $find pour la relation multiple
371
- relationMultiple: { $find: { $eq: ["$relatedValue", 222] } },
372
- // Inclure d'autres champs requis ou avec défauts si nécessaire
373
- stringMaxLength: 'short',
374
- emailField: 'find@example.com',
375
- phoneField: '+19876543210',
376
- urlField: 'https://find.example.com',
377
- numberMinMax: 18,
378
- dateField: '2023-11-20',
379
- dateMinMax: '2023-11-20',
380
- arrayNumber: [10],
381
- fileField: { name: 'find.png', type: 'image/png', size: 1024, guid: 'dummy-find-png' },
382
- colorField: '#123456',
383
- codeJsonField: { "find": true },
384
- codeJsField: 'console.log("find");',
385
- objectField: { find: "test" },
386
- modelNameField: 'findModel',
387
- modelFieldNameField: { model: 'findModel', field: 'findField' }
388
- };
389
-
390
- // 3. Insérer le document principal
391
- const result = await insertData(comprehensiveTestModelDefinition.name, dataWithFindRelations, {}, currentTestUser, false);
392
-
393
- // 4. Vérifier le résultat de l'insertion
394
- expect(result.success, `Insertion with $find failed: ${result.error}`).toBe(true);
395
- expect(result.insertedIds).toHaveLength(1);
396
-
397
- // 5. Récupérer le document inséré depuis la base de données
398
- const insertedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(result.insertedIds[0]) });
399
- expect(insertedDoc).not.toBeNull();
400
-
401
- console.log({insertedDoc: insertedDoc, relatedIdB})
402
- expect(insertedDoc.relationSingle).toBe(relatedIdA.toString());
403
-
404
- expect(insertedDoc.relationMultiple).toBeInstanceOf(Array);
405
- expect(insertedDoc.relationMultiple).toHaveLength(2);
406
- // Convertir les ObjectIds en string pour une comparaison facile
407
- const insertedRelationMultipleIds = insertedDoc.relationMultiple.map(id => id.toString());
408
- expect(insertedRelationMultipleIds).toContain(relatedIdB.toString());
409
- expect(insertedRelationMultipleIds).toContain(relatedIdC.toString());
410
-
411
- // Vérifier d'autres champs pour s'assurer que le reste des données est correct
412
- expect(insertedDoc.stringRequired).toBe('DocWithFind');
413
- expect(insertedDoc.enumField).toBe('beta');
414
-
415
- await purge();
416
- }, 10000); // Augmenter le timeout si nécessaire
417
- });
418
-
419
- describe('editData avec comprehensiveTestModel', async () => {
420
- let docToEditId;
421
- let relatedDocId;
422
-
423
- const initTest = async () => {
424
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
425
- // Insert a base document and a related document to be used in edit tests
426
- const relatedInsert = await insertData(relatedModelDefinition.name, { relatedName: 'RelForEdit', relatedValue: 50 }, {}, currentTestUser, false);
427
- relatedDocId = relatedInsert.insertedIds[0];
428
-
429
- const initialData = {
430
- stringRequired: 'Initial',
431
- stringUnique: 'UniqueForEdit',
432
- enumField: 'alpha',
433
- passwordField: 'initialPass',
434
- relationSingle: relatedDocId
435
- };
436
- const insertResult = await insertData(comprehensiveTestModelDefinition.name, initialData, {}, currentTestUser, false);
437
- docToEditId = insertResult.insertedIds[0];
438
-
439
- return { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge };
440
- };
441
- it('devrait modifier des données valides', async () => {
442
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
443
- const dataToEdit = { stringRequired: 'Edited String', numberMinMax: 12 };
444
- const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, dataToEdit, {}, currentTestUser);
445
-
446
- expect(result.success, `Edit failed: ${result.error}`).toBe(true);
447
- expect(result.modifiedCount).toBe(1);
448
-
449
- const editedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docToEditId) });
450
- expect(editedDoc.stringRequired).toBe('Edited String');
451
- expect(editedDoc.numberMinMax).toBe(12);
452
-
453
- await purge();
454
- });
455
-
456
- it('devrait rejeter une modification avec un string requis vide', async () => {
457
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
458
- const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringRequired: '' }, {}, currentTestUser);
459
- expect(result.success).toBe(false);
460
- expect(result.error).toContain('stringRequired');
461
-
462
- await purge();
463
- });
464
-
465
- it('devrait rejeter une modification avec un number hors des bornes (numberMinMax)', async () => {
466
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
467
- const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringRequired: 'test', numberMinMax: 50 }, {}, currentTestUser);
468
- expect(result.success).toBe(false);
469
- expect(result.error).toContain('numberMinMax');
470
- await purge();
471
- });
472
-
473
- it('devrait rejeter une modification qui viole une contrainte unique (stringUnique)', async ({skip}) => {
474
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
475
- // Insert another doc to create the unique conflict
476
-
477
- await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'Other', stringUnique: 'UniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser, false);
478
- // Insert another doc to create the unique conflict
479
- const res1 = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'Other', stringUnique: 'ExistingUniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser, false);
480
-
481
- const result = await editData(comprehensiveTestModelDefinition.name, { stringUnique: 'ExistingUniqueValue'}, { stringRequired: 'Other', stringUnique: 'UniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser);
482
- expect(result.success).toBe(false);
483
- expect(result.error).toContain('UniqueValue');
484
- expect(result.error).toContain('stringUnique');
485
- await purge();
486
- });
487
-
488
- // TODO: Ajouter plus de tests de validation pour editData, similaires à ceux de insertData
489
- });
490
-
491
- // --- Tests deleteData (repris de votre exemple, peuvent être gardés tels quels ou adaptés) ---
492
- describe('deleteData (tests existants)', () => {
493
- it('supprimer des données par ID (deleteData)', async () => {
494
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
495
- const dataToDelete = { name: 'Objet à supprimer', value: 300 };
496
- // Utiliser le modèle simple pour ces tests delete pour ne pas avoir à remplir tous les champs requis de comprehensive
497
- const simpleModel = { name: 'simpleDeleteModel', _user: currentTestUser.username, description: '', fields: [{ name: 'name', type: 'string' }, { name: 'value', type: 'number' }] };
498
- await testModelsColInstance.insertOne(simpleModel);
499
-
500
- const insertResult = await insertData(simpleModel.name, dataToDelete, {}, currentTestUser, false);
501
- const docId = insertResult.insertedIds[0];
502
-
503
- const deleteResult = await deleteData(simpleModel.name, [docId], currentTestUser);
504
-
505
- expect(deleteResult.success).toBe(true);
506
- expect(deleteResult.deletedCount).toBe(1);
507
-
508
- const deletedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docId) });
509
- expect(deletedDoc).toBeNull();
510
- await purge();
511
- await purge();
512
- });
513
-
514
- it('supprimer des données par filtre (deleteData)', async () => {
515
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
516
- const simpleModel = { name: 'deleteModelTest', _user: currentTestUser.username, description: '',fields: [{ name: 'name', type: 'string' }, { name: 'value', type: 'number' }] };
517
- await testModelsColInstance.insertOne(simpleModel);
518
-
519
- await insertData(simpleModel.name, { name: 'Item Filtrable 1', value: 401 }, {}, currentTestUser, false);
520
- await insertData(simpleModel.name, { name: 'Item Filtrable 2', value: 402 }, {}, currentTestUser, false);
521
- await insertData(simpleModel.name, { name: 'Autre Item', value: 403 }, {}, currentTestUser, false);
522
-
523
- const filter = { $regexMatch: {
524
- input: '$name',
525
- regex: "^Item Filtrable"
526
- } };
527
- const deleteResult = await deleteData(simpleModel.name, filter, currentTestUser);
528
-
529
- expect(deleteResult.success).toBe(true);
530
- expect(deleteResult.deletedCount).toBe(2);
531
-
532
- const remainingCount = await testDatasColInstance.countDocuments({ _user: currentTestUser.username, _model: simpleModel.name, name: { $regex: "^Item Filtrable" } });
533
- expect(remainingCount).toBe(0);
534
- const totalRemaining = await testDatasColInstance.countDocuments({ _user: currentTestUser.username, _model: simpleModel.name });
535
- expect(totalRemaining).toBe(1);
536
- await purge();
537
- });
538
- });
539
-
540
- describe('searchData avec comprehensiveTestModel et filtres $find', async () => {
541
- let docId1, docId2;
542
- let relatedDocId_A1, relatedDocId_A2, relatedDocId_B1;
543
-
544
- const initTest =async () => {
545
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
546
-
547
- // Insérer des documents liés
548
- const relA1 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_A1', relatedValue: 101 }, {}, currentTestUser, false);
549
- relatedDocId_A1 = relA1.insertedIds[0].toString();
550
- const relA2 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_A2', relatedValue: 102 }, {}, currentTestUser, false);
551
- relatedDocId_A2 = relA2.insertedIds[0].toString();
552
- const relB1 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_B1', relatedValue: 201 }, {}, currentTestUser, false);
553
- relatedDocId_B1 = relB1.insertedIds[0].toString();
554
-
555
- // Insérer des documents principaux
556
- const data1 = {
557
- stringRequired: 'SearchDoc1',
558
- stringUnique: 'UniqueSearch1',
559
- enumField: 'alpha',
560
- passwordField: 'searchPass1',
561
- relationSingle: relatedDocId_A1,
562
- relationMultiple: [relatedDocId_A1, relatedDocId_A2],
563
- number: 10
564
- };
565
- const insertResult1 = await insertData(comprehensiveTestModelDefinition.name, data1, {}, currentTestUser, false);
566
- docId1 = insertResult1.insertedIds[0].toString();
567
-
568
- const data2 = {
569
- stringRequired: 'SearchDoc2',
570
- stringUnique: 'UniqueSearch2',
571
- enumField: 'beta',
572
- passwordField: 'searchPass2',
573
- relationSingle: relatedDocId_B1,
574
- relationMultiple: [relatedDocId_B1],
575
- number: 20
576
- };
577
- const insertResult2 = await insertData(comprehensiveTestModelDefinition.name, data2, {}, currentTestUser, false);
578
- docId2 = insertResult2.insertedIds[0].toString();
579
-
580
- return { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge };
581
- };
582
-
583
- it('devrait trouver un document par une condition sur une relation simple ($find)', async () => {
584
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
585
- const searchParams = {
586
- model: comprehensiveTestModelDefinition.name,
587
- filter: {
588
- relationSingle: {
589
- "$find": { "$and": [{"$eq": ["$$this.relatedValue", 101]}] }
590
- }
591
- },
592
- autoExpand: true,
593
- depth: 8
594
- };
595
- const { data, count } = await searchData(searchParams, currentTestUser);
596
- expect(count).toBe(1);
597
- expect(data).toHaveLength(1);
598
- expect(data[0]._id.toString()).toBe(docId1.toString());
599
- // Vérifier que la relation est bien populée si depth > 0
600
- expect(data[0].relationSingle).toBeInstanceOf(Object);
601
- expect(data[0].relationSingle.relatedName).toBe('Rel_Search_A1');
602
- await purge();
603
- });
604
-
605
- it('ne devrait pas trouver de document si la condition $find sur relation simple ne correspond pas', async () => {
606
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
607
- const searchParams = {
608
- model: comprehensiveTestModelDefinition.name,
609
- filter: {
610
- relationSingle: {
611
- "$find": { "$eq": ["$$this.relatedName", "NonExistentRelName"] }
612
- }
613
- },
614
- depth: 1
615
- };
616
- const { data, count } = await searchData(searchParams, currentTestUser);
617
- expect(count).toBe(0);
618
- expect(data).toHaveLength(0);
619
- await purge();
620
- });
621
-
622
- it('devrait trouver des documents par une condition sur une relation multiple ($find)', async () => {
623
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
624
- const searchParams = {
625
- model: comprehensiveTestModelDefinition.name,
626
- filter: {
627
- $and: [{$ne:["$stringRequired", null]}, {
628
- relationMultiple: { // Le champ qui est un tableau de relations
629
- "$find": {"$eq": ["$$this.relatedValue", 102]} // Condition sur les documents liés
630
- }
631
- }]
632
- },
633
- depth: 1
634
- };
635
- const { data, count } = await searchData(searchParams, currentTestUser);
636
- expect(count).toBe(1);
637
- expect(data).toHaveLength(1);
638
- expect(data[0]._id.toString()).toBe(docId1.toString());
639
- // Note: searchData with depth > 0 will populate the relationMultiple array
640
- expect(data[0].relationMultiple).toBeInstanceOf(Array);
641
- expect(data[0].relationMultiple.some(rel => rel.relatedValue === 102)).toBe(true);
642
- await purge();
643
- });
644
-
645
-
646
- it('devrait trouver des documents en combinant un filtre normal et un filtre $find', async () => {
647
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
648
- const searchParams = {
649
- model: comprehensiveTestModelDefinition.name,
650
- filter: {
651
- stringRequired: "SearchDoc1", // Filtre sur le modèle principal
652
- relationSingle: {
653
- "$find": {"$gt": ["$$this.relatedValue", 100] } // Filtre sur la relation
654
- }
655
- },
656
- depth: 1
657
- };
658
- const { data, count } = await searchData(searchParams,currentTestUser);
659
- expect(count).toBe(1);
660
- expect(data).toHaveLength(1);
661
- expect(data[0]._id.toString()).toBe(docId1.toString());
662
- await purge();
663
- });
664
-
665
- it('devrait retourner un tableau vide si $find ne correspond à aucun document lié', async () => {
666
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
667
- const searchParams = {
668
- model: comprehensiveTestModelDefinition.name,
669
- filter: {
670
- relationMultiple: {
671
- "$find": { "$eq": ["$$this.relatedName", "NonExistentRelNameForMultiple"] }
672
- }
673
- },
674
- depth: 1
675
- };
676
- const { data, count } = await searchData(searchParams,currentTestUser);
677
- expect(count).toBe(0);
678
- expect(data).toHaveLength(0);
679
- await purge();
680
- });
681
-
682
- });
683
-
684
- describe('installPack', () => {
685
- let testPacksColInstance;
686
-
687
- beforeAll(() => {
688
- // Note: l'initialisation est déjà faite dans le beforeAll global.
689
- // On s'assure juste d'avoir une référence si on en a besoin spécifiquement ici.
690
- // Si vous n'avez pas suivi l'étape 1, vous pouvez l'initialiser ici.
691
- testPacksColInstance = getCollection('packs');
692
- });
693
-
694
- beforeEach(async () => {
695
- // On supprime les données de ce test
696
- });
697
-
698
- it('devrait installer un pack, créer les modèles et insérer les données avec relations via $link', async () => {
699
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
700
- // 1. Définir le pack de données mock
701
- const mockPack = {
702
- private: false,
703
- name: 'test-pack-for-install',
704
- models: [
705
- {
706
- name: 'packUser',
707
- description: 'Modèle pour les utilisateurs du pack',
708
- fields: [
709
- { name: 'username', type: 'string', required: true, unique: true },
710
- { name: 'level', type: 'number', default: 1 }
711
- ]
712
- },
713
- {
714
- name: 'packItem',
715
- description: 'Modèle pour les items du pack',
716
- fields: [
717
- { name: 'itemName', type: 'string', required: true },
718
- { name: 'owner', type: 'relation', relation: 'packUser' }
719
- ]
720
- }
721
- ],
722
- data: {
723
- all: { // 'all' pour les données non spécifiques à une langue
724
- packUser: [
725
- { username: 'playerOne' },
726
- { username: 'playerTwo', level: 5 }
727
- ],
728
- packItem: [
729
- // La nouvelle syntaxe $link pour résoudre les relations
730
- { itemName: 'Sword', owner: { $link: { _model: 'packUser', username: 'playerOne' } } },
731
- { itemName: 'Shield', owner: { $link: { _model: 'packUser', username: 'playerTwo' } } }
732
- ]
733
- }
734
- }
735
- };
736
-
737
- // 2. Insérer le pack dans la collection pour le rendre disponible au test
738
- const packInsertResult = await testPacksColInstance.insertOne(mockPack);
739
- const packId = packInsertResult.insertedId.toString();
740
-
741
- // 3. Appeler la fonction à tester avec la nouvelle signature
742
- const result = await installPack(packId, currentTestUser, 'en');
743
-
744
- // 4. Assertions sur le résumé de l'installation
745
- expect(result.success, `L'installation du pack a échoué: ${result.errors?.join('; ')}`).toBe(true);
746
- expect(result.summary.models.installed).toHaveLength(2);
747
- expect(result.summary.datas.inserted).toBe(4); // 2 utilisateurs + 2 items
748
- expect(result.summary.datas.updated).toBe(2); // 2 items mis à jour avec la relation
749
-
750
- // 5. Vérifier la création des modèles
751
- const userModel = await testModelsColInstance.findOne({ name: 'packUser', _user: currentTestUser.username });
752
- expect(userModel).not.toBeNull();
753
- const itemModel = await testModelsColInstance.findOne({ name: 'packItem', _user: currentTestUser.username });
754
- expect(itemModel).not.toBeNull();
755
- expect(itemModel.fields.find(f => f.name === 'owner').relation).toBe('packUser');
756
-
757
- // 6. Vérifier l'insertion des données
758
- const insertedUsers = await testDatasColInstance.find({ _model: 'packUser', _user: currentTestUser.username }).toArray();
759
- expect(insertedUsers).toHaveLength(2);
760
- const insertedItems = await testDatasColInstance.find({ _model: 'packItem', _user: currentTestUser.username }).toArray();
761
- expect(insertedItems).toHaveLength(2);
762
-
763
- // 7. Vérifier la résolution des relations via $link
764
- const playerOne = insertedUsers.find(u => u.username === 'playerOne');
765
- const sword = insertedItems.find(i => i.itemName === 'Sword');
766
- expect(sword.owner.toString()).toBe(playerOne._id.toString());
767
-
768
- const playerTwo = insertedUsers.find(u => u.username === 'playerTwo');
769
- const shield = insertedItems.find(i => i.itemName === 'Shield');
770
- expect(shield.owner.toString()).toBe(playerTwo._id.toString());
771
- await purge();
772
- }, 10000);
773
-
774
- it("ne devrait installer que les modèles valides d'un pack et rapporter les erreurs", async () => {
775
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
776
- const mockPackWithInvalidModel = {
777
- name: 'invalid-model-pack',
778
- private: false,
779
- models: [
780
- {
781
- // 'name' est requis, on l'omet pour causer une erreur
782
- description: 'Modèle invalide sans nom',
783
- fields: []
784
- },
785
- { // Un modèle valide pour s'assurer que l'installation continue
786
- name: 'validPackModel',
787
- description: 'Un modèle valide',
788
- fields: [{ name: 'title', type: 'string' }]
789
- }
790
- ],
791
- data: {}
792
- };
793
-
794
- const packInsertResult = await testPacksColInstance.insertOne(mockPackWithInvalidModel);
795
- const packId = packInsertResult.insertedId.toString();
796
-
797
- const result = await installPack(packId, currentTestUser, 'en');
798
-
799
- // Le succès global est faux s'il y a des erreurs
800
- expect(result.success).toBe(false);
801
- expect(result.errors).toHaveLength(1);
802
- expect(result.errors[0]).toContain("Failed to install model 'unknown'"); // Le nom est inconnu
803
-
804
- // Vérifier le résumé de l'option
805
- expect(result.summary.models.failed).toHaveLength(1);
806
- expect(result.summary.models.failed[0]).toBe('unknown');
807
- expect(result.summary.models.installed).toHaveLength(1);
808
- expect(result.summary.models.installed[0]).toBe('validPackModel');
809
-
810
- // Vérifier en BDD que le modèle valide a bien été créé
811
- const validModel = await testModelsColInstance.findOne({ name: 'validPackModel', _user: currentTestUser.username });
812
- expect(validModel).not.toBeNull();
813
- await purge();
814
- });
815
-
816
- it('devrait correctement insérer des données JSON depuis un pack', async () => {
817
- const { currentTestUser } = await setupTestContext();
818
- // 1. Définir le pack avec des données JSON
819
- const mockPackWithJson = {
820
- name: 'test-pack-with-json',
821
- private: false,
822
- models: [
823
- {
824
- name: 'packConfig',
825
- description: 'Modèle avec un champ JSON',
826
- fields: [
827
- { name: 'configName', type: 'string', required: true },
828
- { name: 'settings', type: 'code', language: 'json' }
829
- ]
830
- }
831
- ],
832
- data: {
833
- all: {
834
- packConfig: [
835
- {
836
- configName: 'mainConfig',
837
- settings: {
838
- theme: 'dark',
839
- notifications: {
840
- enabled: true,
841
- level: 'important'
842
- }
843
- }
844
- }
845
- ]
846
- }
847
- }
848
- };
849
-
850
- // 2. Insérer le pack
851
- const packInsertResult = await testPacksColInstance.insertOne(mockPackWithJson);
852
- const packId = packInsertResult.insertedId.toString();
853
-
854
- // 3. Appeler la fonction à tester
855
- const result = await installPack(packId, currentTestUser, 'en');
856
-
857
- // 4. Assertions
858
- expect(result.success, `L'installation du pack JSON a échoué: ${result.errors?.join('; ')}`).toBe(true);
859
- expect(result.summary.models.installed).toHaveLength(1);
860
- expect(result.summary.datas.inserted).toBe(1);
861
-
862
- // 5. Vérifier les données insérées en BDD
863
- const datasCol = await getCollectionForUser(currentTestUser);
864
- const insertedConfig = await datasCol.findOne({ _model: 'packConfig', _user: currentTestUser.username });
865
-
866
- expect(insertedConfig).not.toBeNull();
867
- expect(insertedConfig.configName).toBe('mainConfig');
868
- expect(typeof insertedConfig.settings).toBe('object');
869
- expect(insertedConfig.settings).toEqual({
870
- theme: 'dark',
871
- notifications: {
872
- enabled: true,
873
- level: 'important'
874
- }
875
- });
876
-
877
- await getCollection('packs').deleteMany({ _id: packId});
878
- });
879
- });
880
- // In test/data.integration.test.js
881
-
882
- describe('relationFilter validation', () => {
883
- let user;
884
- let activeProductId, inactiveProductId;
885
-
886
- // Model names are kept in French to match the database, but variables are in English.
887
- const productModel = {
888
- name: 'produitTestFiltre',
889
- description: '',
890
- fields: [
891
- { name: 'name', type: 'string' },
892
- { name: 'actif', type: 'boolean', default: false }
893
- ]
894
- };
895
- const orderModel = {
896
- name: 'commandeTestFiltre',
897
- description: '',
898
- fields: [
899
- { name: 'ref', type: 'string' },
900
- {
901
- name: 'produit',
902
- type: 'relation',
903
- relation: 'produitTestFiltre',
904
- relationFilter: { "$eq":["$actif", true] } // Only link active products
905
- }
906
- ]
907
- };
908
-
909
- // Set up the context once for all tests in this describe block.
910
- beforeAll(async () => {
911
- user = await engine.userProvider.findUserByUsername('demo');
912
- // Cleanup before starting to ensure a clean state
913
- await deleteModels({ name: productModel.name, _user: user.username });
914
- await deleteModels({ name: orderModel.name, _user: user.username });
915
- await deleteData(productModel.name, {}, user);
916
- await deleteData(orderModel.name, {}, user);
917
-
918
- // Create models
919
- await createModel({ ...productModel, _user: user.username });
920
- await createModel({ ...orderModel, _user: user.username });
921
-
922
- // Create test data
923
- const activeProduct = await insertData(productModel.name, { name: 'Active Product', actif: true }, {}, user);
924
- const inactiveProduct = await insertData(productModel.name, { name: 'Inactive Product', actif: false }, {}, user);
925
-
926
- activeProductId = activeProduct.insertedIds[0];
927
- inactiveProductId = inactiveProduct.insertedIds[0];
928
- });
929
-
930
- // Clean up everything after all tests in this block have run.
931
- afterAll(async () => {
932
- await deleteModels({ name: productModel.name, _user: user.username });
933
- await deleteModels({ name: orderModel.name, _user: user.username });
934
- await deleteData(productModel.name, {}, user);
935
- await deleteData(orderModel.name, {}, user);
936
- const coll = await getCollectionForUser(user);
937
- await coll.drop();
938
- });
939
-
940
- it('should ALLOW inserting data with a valid relation', async () => {
941
- const result = await insertData(orderModel.name, { ref: 'CMD-OK', produit: activeProductId }, {}, user);
942
- expect(result.success).toBe(true);
943
- expect(result.insertedIds).toHaveLength(1);
944
- // Cleanup the created order for test isolation
945
- await deleteData(orderModel.name, result.insertedIds, user);
946
- });
947
-
948
- it('should REJECT inserting data with a relation that does not respect the filter', async () => {
949
- const result = await insertData(orderModel.name, { ref: 'CMD-FAIL', produit: inactiveProductId }, {}, user);
950
- expect(result.success).toBe(false);
951
- expect(result.error).toContain('produit');
952
- });
953
-
954
- it('should ALLOW updating data with a valid relation', async () => {
955
- // First, create a valid order
956
- const initialOrder = await insertData(orderModel.name, { ref: 'CMD-TO-EDIT', produit: activeProductId }, {}, user);
957
-
958
- // Update it (even with the same value, this tests the code path)
959
- const result = await editData(orderModel.name, { _id: initialOrder.insertedIds[0] }, { produit: activeProductId }, {}, user);
960
-
961
- expect(result.success).toBe(true);
962
- // The hash might not change if only metadata like _updatedAt changes, so modifiedCount can be 0 or 1.
963
- expect(result.modifiedCount).toBeGreaterThanOrEqual(0);
964
-
965
- // Cleanup
966
- await deleteData(orderModel.name, initialOrder.insertedIds, user);
967
- });
968
-
969
- it('should REJECT updating data with a relation that does not respect the filter', async () => {
970
- // First, create a valid order
971
- const initialOrder = await insertData(orderModel.name, { ref: 'CMD-TO-EDIT-FAIL', produit: activeProductId }, {}, user);
972
-
973
- // Attempt to link it to an inactive product, expecting it to fail.
974
- const result = await editData(orderModel.name, { _id: initialOrder.insertedIds[0] }, { produit: inactiveProductId }, {}, user);
975
- expect(result.success).toBe(false);
976
- expect(result.error).toContain('produit');
977
- // Cleanup
978
- await deleteData(orderModel.name, initialOrder.insertedIds, user);
979
- });
980
- });
981
-
1
+ // __tests__/data.integration.test.js
2
+ import { ObjectId } from 'mongodb';
3
+ import {vi, expect, describe, it, afterEach,beforeEach, beforeAll, afterAll} from 'vitest';
4
+ import { Config } from '../src/config.js';
5
+ import {
6
+ insertData,
7
+ editData,
8
+ deleteData,
9
+ searchData, installPack, deleteModels, createModel, patchData
10
+ } from '../src/index.js';
11
+
12
+ import {
13
+ modelsCollection as getAppModelsCollection,
14
+ getCollection,
15
+ getCollectionForUser as getAppUserCollection, getCollectionForUser
16
+ } from '../src/modules/mongodb.js';
17
+ import {getRandom} from "../src/core.js";
18
+ import {generateUniqueName, initEngine} from "../src/setenv.js";
19
+ import {purgeData} from "../src/modules/data/data.history.js";
20
+
21
+ let testModelsColInstance;
22
+ let testDatasColInstance;
23
+
24
+ // Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
25
+ async function setupTestContext() {
26
+
27
+ const currentTestModelName = generateUniqueName('relatedModel');
28
+ const currentRelatedModelName = generateUniqueName('comprehensiveModel');
29
+
30
+ // Créer un utilisateur unique pour ce test
31
+ const currentTestUser = {
32
+ username: generateUniqueName('testuserDataIntegration'),
33
+ userPlan: 'premium',
34
+ email: generateUniqueName('test') + '@example.com'
35
+ };
36
+
37
+ // Initialize collection instances after the engine is ready
38
+ testModelsColInstance = getAppModelsCollection;
39
+ testDatasColInstance = await getAppUserCollection(currentTestUser);
40
+
41
+ const relatedModelDefinition = {
42
+ name: currentRelatedModelName,
43
+ _user: currentTestUser.username,
44
+ description: 'Model for testing relations',
45
+ fields: [
46
+ {name: 'relatedName', type: 'string', required: true, unique: true},
47
+ {name: 'relatedValue', type: 'number'}
48
+ ],
49
+ maxRequestData: 10
50
+ };
51
+
52
+ const comprehensiveTestModelDefinition = {
53
+ name: currentTestModelName,
54
+ _user: currentTestUser.username,
55
+ description: 'A model with all field types and constraints for testing',
56
+ fields: [
57
+ // String types
58
+ {name: 'stringRequired', type: 'string', required: true, hint: 'A required string'},
59
+ {name: 'stringUnique', type: 'string', unique: true, hint: 'A unique string'},
60
+ {name: 'stringMaxLength', type: 'string', maxlength: 5, hint: 'String with max length 5'},
61
+ {name: 'stringDefault', type: 'string', default: 'defaultString'},
62
+ {name: 'string_tLang', type: 'string_t', hint: 'Localizable string'},
63
+ {name: 'richtextField', type: 'richtext', maxlength: 200},
64
+ {name: 'passwordField', type: 'password'},
65
+ {name: 'emailField', type: 'email'},
66
+ {name: 'phoneField', type: 'phone'},
67
+ {name: 'urlField', type: 'url'},
68
+ // Number types
69
+ {name: 'number', type: 'number'},
70
+ {name: 'numberMinMax', type: 'number', min: 10, max: 20},
71
+ {name: 'numberStep', type: 'number', step: 0.5},
72
+ {name: 'numberDefault', type: 'number', default: 42},
73
+ // Boolean
74
+ {name: 'booleanField', type: 'boolean'},
75
+ {name: 'booleanDefault', type: 'boolean', default: true},
76
+ // Date & Datetime
77
+ {name: 'dateField', type: 'date'},
78
+ {name: 'datetimeField', type: 'datetime', default: 'now'},
79
+ {name: 'dateMinMax', type: 'date', min: '2023-01-01', max: '2023-12-31'},
80
+ // Enum
81
+ {name: 'enumField', type: 'enum', items: ['alpha', 'beta', 'gamma']},
82
+ {name: 'enumDefault', type: 'enum', items: ['one', 'two'], default: 'one'},
83
+ // Array
84
+ {name: 'arrayString', type: 'array', itemsType: 'string', maxItems: 2},
85
+ {name: 'arrayNumber', type: 'array', itemsType: 'number', minItems: 1},
86
+ {name: 'arrayEnum', type: 'array', itemsType: 'enum', items: ['a', 'b']},
87
+ // Relation
88
+ {name: 'relationSingle', type: 'relation', relation: currentRelatedModelName},
89
+ {name: 'relationMultiple', type: 'relation', relation: currentRelatedModelName, multiple: true},
90
+ // File (metadata validation)
91
+ {name: 'fileField', type: 'file', mimeTypes: ['image/png', 'application/pdf'], maxSize: 1024 * 10}, // 10KB
92
+ // Color
93
+ {name: 'colorField', type: 'color'},
94
+ // Code
95
+ {name: 'codeJsonField', type: 'code', language: 'json'},
96
+ {name: 'codeJsField', type: 'code', language: 'javascript', maxlength: 100},
97
+ // Object (simple validation, structure not deeply validated by default dataTypes.object)
98
+ {name: 'objectField', type: 'object'},
99
+ // Model & ModelField (validation of string format, not existence)
100
+ {name: 'modelNameField', type: 'model'},
101
+ {name: 'modelFieldNameField', type: 'modelField'} // Note: modelField type expects an object {model: 'modelName', field: 'fieldName'}
102
+ ],
103
+ maxRequestData: 50
104
+ };
105
+
106
+ if( await testModelsColInstance.find({ $or: [{name: comprehensiveTestModelDefinition.name}, {name: relatedModelDefinition.name}]})) {
107
+ // Insérer les modèles en base
108
+ await testModelsColInstance.insertMany([
109
+ comprehensiveTestModelDefinition,
110
+ relatedModelDefinition
111
+ ]);
112
+ }
113
+ await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
114
+
115
+ const purge = async ()=>{
116
+ await purgeData(currentTestUser);
117
+ }
118
+
119
+ // Retourner toutes les variables nécessaires pour un test
120
+ return {
121
+ currentTestUser,
122
+ comprehensiveTestModelDefinition,
123
+ relatedModelDefinition,
124
+ purge
125
+ };
126
+ }
127
+
128
+ let engine;
129
+ describe('Intégration des fonctions CRUD de données avec validation complète', () => {
130
+
131
+ beforeAll(async () =>{
132
+ Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
133
+ engine = await initEngine();
134
+
135
+ })
136
+ describe('insertData avec comprehensiveTestModel', () => {
137
+ it('devrait insérer des données valides pour tous les types de champs', async () => {
138
+
139
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
140
+ // 1. Créer un suffixe unique pour cette exécution de test
141
+ const testSuffix = getRandom(1000, 9999); // Génère un nombre aléatoire
142
+
143
+ // 2. Insérer les documents liés avec des noms uniques garantis
144
+ const relatedDoc1 = await insertData(relatedModelDefinition.name, {
145
+ relatedName: `Rel1_Unique_${testSuffix}`, // Nom unique
146
+ relatedValue: 100
147
+ }, {}, currentTestUser, false);
148
+
149
+ const relatedDoc2 = await insertData(relatedModelDefinition.name, {
150
+ relatedName: `Rel2_Unique_${testSuffix}`, // Nom unique
151
+ relatedValue: 200
152
+ }, {}, currentTestUser, false);
153
+
154
+ // S'assurer que les insertions précédentes ont réussi avant de continuer
155
+ expect(relatedDoc1.success, "L'insertion du document lié 1 a échoué").toBe(true);
156
+ expect(relatedDoc2.success, "L'insertion du document lié 2 a échoué").toBe(true);
157
+
158
+ // --- FIN DU SETUP AMÉLIORÉ ---
159
+
160
+ const validData = {
161
+ stringRequired: 'Valid String',
162
+ stringUnique: `UniqueValueForInsert_${testSuffix}`, // Valeur unique garantie
163
+ stringMaxLength: '12345',
164
+ string_tLang: 'Bonjour',
165
+ richtextField: '<p>Valid rich text</p>',
166
+ passwordField: 'validPassword123',
167
+ emailField: 'valid@example.com',
168
+ phoneField: '+1234567890',
169
+ urlField: 'https://example.com',
170
+ number: 123,
171
+ numberMinMax: 15,
172
+ numberStep: 10.5,
173
+ booleanField: true,
174
+ dateField: '2023-07-15',
175
+ dateMinMax: '2023-06-01',
176
+ enumField: 'alpha',
177
+ arrayString: ['one', 'two'],
178
+ arrayNumber: [1, 2, 3],
179
+ arrayEnum: ['a'],
180
+ relationSingle: relatedDoc1.insertedIds[0].toString(),
181
+ relationMultiple: [relatedDoc1.insertedIds[0].toString(), relatedDoc2.insertedIds[0].toString()],
182
+ // Le mock de fichier est OK, pas besoin de le rendre unique
183
+ fileField: { name: 'test.png', type: 'image/png', size: 1024 * 5, guid: `dummy-guid-png-${testSuffix}` },
184
+ colorField: '#FF0000',
185
+ codeJsonField: { "key": "value" },
186
+ codeJsField: 'console.log("hello");',
187
+ objectField: { a: 1, b: "test" },
188
+ modelNameField: 'someModelName',
189
+ modelFieldNameField: { model: 'someModelName', field: 'someFieldName' }
190
+ };
191
+
192
+ const result = await insertData(comprehensiveTestModelDefinition.name, validData, {}, currentTestUser, false);
193
+ expect(result.success, `L'insertion principale a échoué: ${result.error}`).toBe(true);
194
+ expect(result.insertedIds).toHaveLength(1);
195
+
196
+ console.log(result);
197
+ const insertedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(result.insertedIds[0]) });
198
+ expect(insertedDoc).not.toBeNull();
199
+ expect(insertedDoc.stringRequired).toBe('Valid String');
200
+ expect(insertedDoc.stringUnique).toBe(`UniqueValueForInsert_${testSuffix}`); // Vérifier la valeur unique
201
+ expect(insertedDoc.stringDefault).toBe('defaultString');
202
+ expect(new Date(insertedDoc.datetimeField).getFullYear()).toBe(new Date().getFullYear());
203
+ expect(insertedDoc.booleanDefault).toBe(true);
204
+ expect(insertedDoc.relationSingle.toString()).toBe(relatedDoc1.insertedIds[0].toString());
205
+
206
+ await purge();
207
+ }, 10000);
208
+
209
+ describe('Validations des champs à l\'insertion', () => {
210
+ // --- STRING VALIDATIONS ---
211
+ it('devrait rejeter un string requis non fourni', async () => {
212
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
213
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringUnique: 'test' /* stringRequired manquant */ }, {}, currentTestUser, false);
214
+ expect(result.success).toBe(false);
215
+ expect(result.error).toContain('stringRequired');
216
+ await purge();
217
+ });
218
+
219
+ it('devrait rejeter un string trop long pour maxlength', async () => {
220
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
221
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', stringMaxLength: '123456' }, {}, currentTestUser, false);
222
+ expect(result.success).toBe(false);
223
+ expect(result.error).toContain('stringMaxLength');
224
+ await purge();
225
+ });
226
+
227
+ it('devrait rejeter une valeur dupliquée pour un champ unique (stringUnique)', async () => {
228
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
229
+ await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req1', stringUnique: 'duplicateMe', 'passwordField': 'pass', number: 1, enumField: 'test' }, {}, currentTestUser, false);
230
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req2', stringUnique: 'duplicateMe', 'passwordField': 'pass', number: 1, enumField: 'test' }, {}, currentTestUser, false);
231
+ expect(result.success).toBe(false);
232
+ await purge();
233
+ });
234
+
235
+ // --- NUMBER VALIDATIONS ---
236
+ it('devrait rejeter un number non numérique', async () => {
237
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
238
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', number: 'not-a-number' }, {}, currentTestUser, false);
239
+ expect(result.success).toBe(false);
240
+ expect(result.error).toContain('number');
241
+ await purge();
242
+ });
243
+
244
+ it('devrait rejeter un number infur à min (numberMinMax)', async () => {
245
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
246
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', numberMinMax: 5 }, {}, currentTestUser, false);
247
+ expect(result.success).toBe(false);
248
+ expect(result.error).toContain('numberMinMax');
249
+ await purge();
250
+ });
251
+
252
+ it('devrait rejeter un number supérieur à max (numberMinMax)', async () => {
253
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
254
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', numberMinMax: 25 }, {}, currentTestUser, false);
255
+ expect(result.success).toBe(false);
256
+ expect(result.error).toContain('numberMinMax');
257
+ await purge();
258
+ });
259
+
260
+ // --- ARRAY VALIDATIONS ---
261
+ it('devrait rejeter un array avec trop d\'items (arrayString maxItems: 2)', async () => {
262
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
263
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayString: ['a', 'b', 'c'] }, {}, currentTestUser, false);
264
+ expect(result.success).toBe(false);
265
+ expect(result.error).toContain('arrayString');
266
+ await purge();
267
+ });
268
+
269
+ it('devrait rejeter un array avec pas assez d\'items (arrayNumber minItems: 1)', async () => {
270
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
271
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayNumber: [] }, {}, currentTestUser, false);
272
+ expect(result.success).toBe(false);
273
+ expect(result.error).toContain('arrayNumber');
274
+ await purge();
275
+ });
276
+
277
+ it('devrait rejeter un array avec un item de type incorrect (arrayNumber)', async () => {
278
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
279
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayNumber: [1, 'not-a-number', 3] }, {}, currentTestUser, false);
280
+ expect(result.success).toBe(false);
281
+ expect(result.error).toContain('arrayNumber');
282
+ await purge();
283
+ });
284
+
285
+ // --- FILE VALIDATIONS (metadata) ---
286
+ it('devrait rejeter un file avec un mimeType non autorisé', async () => {
287
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
288
+ const mockFile = { name: 'test.txt', type: 'text/plain', size: 1024, guid: 'dummy-txt' };
289
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
290
+ expect(result.success).toBe(false);
291
+ expect(result.error).toContain('image/png'); // Ou un message plus spécifique sur mimeTypes
292
+ expect(result.error).toContain('text/plain'); // Ou un message plus spécifique sur mimeTypes
293
+ await purge();
294
+ });
295
+
296
+ it('devrait rejeter un file avec une taille supérieure à maxSize', async () => {
297
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
298
+ const mockFile = { name: 'large.png', type: 'image/png', size: 1024 * 15, guid: 'dummy-large' }; // 15KB > 10KB
299
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
300
+ expect(result.success).toBe(false);
301
+ expect(result.error).toContain('fileField'); // Ou un message plus spécifique sur maxSize
302
+ await purge();
303
+ });
304
+
305
+ // --- EMAIL, URL, PHONE, COLOR (basic format) ---
306
+ it('devrait rejeter un email invalide', async () => {
307
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
308
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', emailField: 'not-an-email' }, {}, currentTestUser, false);
309
+ expect(result.success).toBe(false);
310
+ expect(result.error).toContain('emailField');
311
+ await purge();
312
+ });
313
+ it('devrait rejeter une URL invalide', async () => {
314
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
315
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', urlField: 'not a url' }, {}, currentTestUser, false);
316
+ expect(result.success).toBe(false);
317
+ expect(result.error).toContain('urlField');
318
+ await purge();
319
+ });
320
+ it('devrait rejeter une couleur invalide', async () => {
321
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
322
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', colorField: '#GGHHII' }, {}, currentTestUser, false);
323
+ expect(result.success).toBe(false);
324
+ expect(result.error).toContain('colorField');
325
+ await purge();
326
+ });
327
+
328
+ // --- DATE / DATETIME VALIDATIONS ---
329
+ it('devrait rejeter une date invalide (format)', async () => {
330
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
331
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', dateField: 'not-a-date' }, {}, currentTestUser, false);
332
+ expect(result.success).toBe(false);
333
+ expect(result.error).toContain('dateField');
334
+ await purge();
335
+ });
336
+ it('devrait rejeter une date en dehors de min/max (dateMinMax)', async () => {
337
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
338
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', dateMinMax: '2024-01-01' }, {}, currentTestUser, false);
339
+ expect(result.success).toBe(false);
340
+ expect(result.error).toContain('dateMinMax');
341
+ await purge();
342
+ });
343
+
344
+ // TODO: Ajouter des tests pour les autres types et contraintes (password, richtext, code, object, model, modelField)
345
+ // Pour 'code' avec language 'json', tester l'insertion d'un JSON invalide si la validation va jusque-là.
346
+ // Pour 'relation', tester l'insertion d'un ObjectId non existant (si la validation vérifie l'existence).
347
+ });
348
+
349
+ // --- NOUVEAU TEST POUR $find ---
350
+ it('devrait insérer des données avec des relations référencées par $find', async () => {
351
+ // 1. Insérer les documents liés qui seront trouvés par $find
352
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
353
+ const relatedDocA = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeA', relatedValue: 111 }, {}, currentTestUser, false);
354
+ const relatedDocB = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeB', relatedValue: 222 }, {}, currentTestUser, false);
355
+ const relatedDocC = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeC', relatedValue: 222 }, {}, currentTestUser, false);
356
+
357
+ const relatedIdA = relatedDocA.insertedIds[0];
358
+ const relatedIdB = relatedDocB.insertedIds[0];
359
+ const relatedIdC = relatedDocC.insertedIds[0];
360
+
361
+ // 2. Prr les données du document principal utilisant $find
362
+ const dataWithFindRelations = {
363
+ stringRequired: 'DocWithFind',
364
+ stringUnique: 'UniqueFindValue',
365
+ passwordField: 'findPass',
366
+ enumField: 'beta',
367
+ number: 99,
368
+ // Utiliser $find pour la relation simple
369
+ relationSingle: { $find: { $eq: ["$relatedName", 'FindMeA'] } },
370
+ // Utiliser un tableau de $find pour la relation multiple
371
+ relationMultiple: { $find: { $eq: ["$relatedValue", 222] } },
372
+ // Inclure d'autres champs requis ou avec défauts si nécessaire
373
+ stringMaxLength: 'short',
374
+ emailField: 'find@example.com',
375
+ phoneField: '+19876543210',
376
+ urlField: 'https://find.example.com',
377
+ numberMinMax: 18,
378
+ dateField: '2023-11-20',
379
+ dateMinMax: '2023-11-20',
380
+ arrayNumber: [10],
381
+ fileField: { name: 'find.png', type: 'image/png', size: 1024, guid: 'dummy-find-png' },
382
+ colorField: '#123456',
383
+ codeJsonField: { "find": true },
384
+ codeJsField: 'console.log("find");',
385
+ objectField: { find: "test" },
386
+ modelNameField: 'findModel',
387
+ modelFieldNameField: { model: 'findModel', field: 'findField' }
388
+ };
389
+
390
+ // 3. Insérer le document principal
391
+ const result = await insertData(comprehensiveTestModelDefinition.name, dataWithFindRelations, {}, currentTestUser, false);
392
+
393
+ // 4. Vérifier le résultat de l'insertion
394
+ expect(result.success, `Insertion with $find failed: ${result.error}`).toBe(true);
395
+ expect(result.insertedIds).toHaveLength(1);
396
+
397
+ // 5. Récupérer le document inséré depuis la base de données
398
+ const insertedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(result.insertedIds[0]) });
399
+ expect(insertedDoc).not.toBeNull();
400
+
401
+ console.log({insertedDoc: insertedDoc, relatedIdB})
402
+ expect(insertedDoc.relationSingle).toBe(relatedIdA.toString());
403
+
404
+ expect(insertedDoc.relationMultiple).toBeInstanceOf(Array);
405
+ expect(insertedDoc.relationMultiple).toHaveLength(2);
406
+ // Convertir les ObjectIds en string pour une comparaison facile
407
+ const insertedRelationMultipleIds = insertedDoc.relationMultiple.map(id => id.toString());
408
+ expect(insertedRelationMultipleIds).toContain(relatedIdB.toString());
409
+ expect(insertedRelationMultipleIds).toContain(relatedIdC.toString());
410
+
411
+ // Vérifier d'autres champs pour s'assurer que le reste des données est correct
412
+ expect(insertedDoc.stringRequired).toBe('DocWithFind');
413
+ expect(insertedDoc.enumField).toBe('beta');
414
+
415
+ await purge();
416
+ }, 10000); // Augmenter le timeout si nécessaire
417
+ });
418
+
419
+ describe('editData avec comprehensiveTestModel', async () => {
420
+ let docToEditId;
421
+ let relatedDocId;
422
+
423
+ const initTest = async () => {
424
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
425
+ // Insert a base document and a related document to be used in edit tests
426
+ const relatedInsert = await insertData(relatedModelDefinition.name, { relatedName: 'RelForEdit', relatedValue: 50 }, {}, currentTestUser, false);
427
+ relatedDocId = relatedInsert.insertedIds[0];
428
+
429
+ const initialData = {
430
+ stringRequired: 'Initial',
431
+ stringUnique: 'UniqueForEdit',
432
+ enumField: 'alpha',
433
+ passwordField: 'initialPass',
434
+ relationSingle: relatedDocId
435
+ };
436
+ const insertResult = await insertData(comprehensiveTestModelDefinition.name, initialData, {}, currentTestUser, false);
437
+ docToEditId = insertResult.insertedIds[0];
438
+
439
+ return { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge };
440
+ };
441
+ it('devrait modifier des données valides', async () => {
442
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
443
+ const dataToEdit = { stringRequired: 'Edited String', numberMinMax: 12 };
444
+ const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, dataToEdit, {}, currentTestUser);
445
+
446
+ expect(result.success, `Edit failed: ${result.error}`).toBe(true);
447
+ expect(result.modifiedCount).toBe(1);
448
+
449
+ const editedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docToEditId) });
450
+ expect(editedDoc.stringRequired).toBe('Edited String');
451
+ expect(editedDoc.numberMinMax).toBe(12);
452
+
453
+ await purge();
454
+ });
455
+
456
+ it('devrait rejeter une modification avec un string requis vide', async () => {
457
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
458
+ const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringRequired: '' }, {}, currentTestUser);
459
+ expect(result.success).toBe(false);
460
+ expect(result.error).toContain('stringRequired');
461
+
462
+ await purge();
463
+ });
464
+
465
+ it('devrait rejeter une modification avec un number hors des bornes (numberMinMax)', async () => {
466
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
467
+ const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringRequired: 'test', numberMinMax: 50 }, {}, currentTestUser);
468
+ expect(result.success).toBe(false);
469
+ expect(result.error).toContain('numberMinMax');
470
+ await purge();
471
+ });
472
+
473
+ it('devrait rejeter une modification qui viole une contrainte unique (stringUnique)', async ({skip}) => {
474
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
475
+ // Insert another doc to create the unique conflict
476
+
477
+ await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'Other', stringUnique: 'UniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser, false);
478
+ // Insert another doc to create the unique conflict
479
+ const res1 = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'Other', stringUnique: 'ExistingUniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser, false);
480
+
481
+ const result = await editData(comprehensiveTestModelDefinition.name, { stringUnique: 'ExistingUniqueValue'}, { stringRequired: 'Other', stringUnique: 'UniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser);
482
+ expect(result.success).toBe(false);
483
+ expect(result.error).toContain('UniqueValue');
484
+ expect(result.error).toContain('stringUnique');
485
+ await purge();
486
+ });
487
+
488
+ // TODO: Ajouter plus de tests de validation pour editData, similaires à ceux de insertData
489
+ });
490
+
491
+ // --- Tests deleteData (repris de votre exemple, peuvent être gardés tels quels ou adaptés) ---
492
+ describe('deleteData (tests existants)', () => {
493
+ it('supprimer des données par ID (deleteData)', async () => {
494
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
495
+ const dataToDelete = { name: 'Objet à supprimer', value: 300 };
496
+ // Utiliser le modèle simple pour ces tests delete pour ne pas avoir à remplir tous les champs requis de comprehensive
497
+ const simpleModel = { name: 'simpleDeleteModel', _user: currentTestUser.username, description: '', fields: [{ name: 'name', type: 'string' }, { name: 'value', type: 'number' }] };
498
+ await testModelsColInstance.insertOne(simpleModel);
499
+
500
+ const insertResult = await insertData(simpleModel.name, dataToDelete, {}, currentTestUser, false);
501
+ const docId = insertResult.insertedIds[0];
502
+
503
+ const deleteResult = await deleteData(simpleModel.name, [docId], currentTestUser);
504
+
505
+ expect(deleteResult.success).toBe(true);
506
+ expect(deleteResult.deletedCount).toBe(1);
507
+
508
+ const deletedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docId) });
509
+ expect(deletedDoc).toBeNull();
510
+ await purge();
511
+ await purge();
512
+ });
513
+
514
+ it('supprimer des données par filtre (deleteData)', async () => {
515
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
516
+ const simpleModel = { name: 'deleteModelTest', _user: currentTestUser.username, description: '',fields: [{ name: 'name', type: 'string' }, { name: 'value', type: 'number' }] };
517
+ await testModelsColInstance.insertOne(simpleModel);
518
+
519
+ await insertData(simpleModel.name, { name: 'Item Filtrable 1', value: 401 }, {}, currentTestUser, false);
520
+ await insertData(simpleModel.name, { name: 'Item Filtrable 2', value: 402 }, {}, currentTestUser, false);
521
+ await insertData(simpleModel.name, { name: 'Autre Item', value: 403 }, {}, currentTestUser, false);
522
+
523
+ const filter = { $regexMatch: {
524
+ input: '$name',
525
+ regex: "^Item Filtrable"
526
+ } };
527
+ const deleteResult = await deleteData(simpleModel.name, filter, currentTestUser);
528
+
529
+ expect(deleteResult.success).toBe(true);
530
+ expect(deleteResult.deletedCount).toBe(2);
531
+
532
+ const remainingCount = await testDatasColInstance.countDocuments({ _user: currentTestUser.username, _model: simpleModel.name, name: { $regex: "^Item Filtrable" } });
533
+ expect(remainingCount).toBe(0);
534
+ const totalRemaining = await testDatasColInstance.countDocuments({ _user: currentTestUser.username, _model: simpleModel.name });
535
+ expect(totalRemaining).toBe(1);
536
+ await purge();
537
+ });
538
+ });
539
+
540
+ describe('searchData avec comprehensiveTestModel et filtres $find', async () => {
541
+ let docId1, docId2;
542
+ let relatedDocId_A1, relatedDocId_A2, relatedDocId_B1;
543
+
544
+ const initTest =async () => {
545
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
546
+
547
+ // Insérer des documents liés
548
+ const relA1 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_A1', relatedValue: 101 }, {}, currentTestUser, false);
549
+ relatedDocId_A1 = relA1.insertedIds[0].toString();
550
+ const relA2 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_A2', relatedValue: 102 }, {}, currentTestUser, false);
551
+ relatedDocId_A2 = relA2.insertedIds[0].toString();
552
+ const relB1 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_B1', relatedValue: 201 }, {}, currentTestUser, false);
553
+ relatedDocId_B1 = relB1.insertedIds[0].toString();
554
+
555
+ // Insérer des documents principaux
556
+ const data1 = {
557
+ stringRequired: 'SearchDoc1',
558
+ stringUnique: 'UniqueSearch1',
559
+ enumField: 'alpha',
560
+ passwordField: 'searchPass1',
561
+ relationSingle: relatedDocId_A1,
562
+ relationMultiple: [relatedDocId_A1, relatedDocId_A2],
563
+ number: 10
564
+ };
565
+ const insertResult1 = await insertData(comprehensiveTestModelDefinition.name, data1, {}, currentTestUser, false);
566
+ docId1 = insertResult1.insertedIds[0].toString();
567
+
568
+ const data2 = {
569
+ stringRequired: 'SearchDoc2',
570
+ stringUnique: 'UniqueSearch2',
571
+ enumField: 'beta',
572
+ passwordField: 'searchPass2',
573
+ relationSingle: relatedDocId_B1,
574
+ relationMultiple: [relatedDocId_B1],
575
+ number: 20
576
+ };
577
+ const insertResult2 = await insertData(comprehensiveTestModelDefinition.name, data2, {}, currentTestUser, false);
578
+ docId2 = insertResult2.insertedIds[0].toString();
579
+
580
+ return { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge };
581
+ };
582
+
583
+ it('devrait trouver un document par une condition sur une relation simple ($find)', async () => {
584
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
585
+ const searchParams = {
586
+ model: comprehensiveTestModelDefinition.name,
587
+ filter: {
588
+ relationSingle: {
589
+ "$find": { "$and": [{"$eq": ["$$this.relatedValue", 101]}] }
590
+ }
591
+ },
592
+ autoExpand: true,
593
+ depth: 8
594
+ };
595
+ const { data, count } = await searchData(searchParams, currentTestUser);
596
+ expect(count).toBe(1);
597
+ expect(data).toHaveLength(1);
598
+ expect(data[0]._id.toString()).toBe(docId1.toString());
599
+ // Vérifier que la relation est bien populée si depth > 0
600
+ expect(data[0].relationSingle).toBeInstanceOf(Object);
601
+ expect(data[0].relationSingle.relatedName).toBe('Rel_Search_A1');
602
+ await purge();
603
+ });
604
+
605
+ it('ne devrait pas trouver de document si la condition $find sur relation simple ne correspond pas', async () => {
606
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
607
+ const searchParams = {
608
+ model: comprehensiveTestModelDefinition.name,
609
+ filter: {
610
+ relationSingle: {
611
+ "$find": { "$eq": ["$$this.relatedName", "NonExistentRelName"] }
612
+ }
613
+ },
614
+ depth: 1
615
+ };
616
+ const { data, count } = await searchData(searchParams, currentTestUser);
617
+ expect(count).toBe(0);
618
+ expect(data).toHaveLength(0);
619
+ await purge();
620
+ });
621
+
622
+ it('devrait trouver des documents par une condition sur une relation multiple ($find)', async () => {
623
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
624
+ const searchParams = {
625
+ model: comprehensiveTestModelDefinition.name,
626
+ filter: {
627
+ $and: [{$ne:["$stringRequired", null]}, {
628
+ relationMultiple: { // Le champ qui est un tableau de relations
629
+ "$find": {"$eq": ["$$this.relatedValue", 102]} // Condition sur les documents liés
630
+ }
631
+ }]
632
+ },
633
+ depth: 1
634
+ };
635
+ const { data, count } = await searchData(searchParams, currentTestUser);
636
+ expect(count).toBe(1);
637
+ expect(data).toHaveLength(1);
638
+ expect(data[0]._id.toString()).toBe(docId1.toString());
639
+ // Note: searchData with depth > 0 will populate the relationMultiple array
640
+ expect(data[0].relationMultiple).toBeInstanceOf(Array);
641
+ expect(data[0].relationMultiple.some(rel => rel.relatedValue === 102)).toBe(true);
642
+ await purge();
643
+ });
644
+
645
+
646
+ it('devrait trouver des documents en combinant un filtre normal et un filtre $find', async () => {
647
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
648
+ const searchParams = {
649
+ model: comprehensiveTestModelDefinition.name,
650
+ filter: {
651
+ stringRequired: "SearchDoc1", // Filtre sur le modèle principal
652
+ relationSingle: {
653
+ "$find": {"$gt": ["$$this.relatedValue", 100] } // Filtre sur la relation
654
+ }
655
+ },
656
+ depth: 1
657
+ };
658
+ const { data, count } = await searchData(searchParams,currentTestUser);
659
+ expect(count).toBe(1);
660
+ expect(data).toHaveLength(1);
661
+ expect(data[0]._id.toString()).toBe(docId1.toString());
662
+ await purge();
663
+ });
664
+
665
+ it('devrait retourner un tableau vide si $find ne correspond à aucun document lié', async () => {
666
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
667
+ const searchParams = {
668
+ model: comprehensiveTestModelDefinition.name,
669
+ filter: {
670
+ relationMultiple: {
671
+ "$find": { "$eq": ["$$this.relatedName", "NonExistentRelNameForMultiple"] }
672
+ }
673
+ },
674
+ depth: 1
675
+ };
676
+ const { data, count } = await searchData(searchParams,currentTestUser);
677
+ expect(count).toBe(0);
678
+ expect(data).toHaveLength(0);
679
+ await purge();
680
+ });
681
+
682
+ // --- NOUVEAUX TESTS POUR LE SHORTHAND $find ---
683
+
684
+ it('devrait trouver un document en utilisant un filtre $find simple (shorthand)', async () => {
685
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
686
+ const searchParams = {
687
+ model: comprehensiveTestModelDefinition.name,
688
+ filter: {
689
+ relationSingle: {
690
+ "$find": { "relatedValue": 101 } // Utilisation du shorthand { champ: valeur }
691
+ }
692
+ },
693
+ depth: 1
694
+ };
695
+ const { data, count } = await searchData(searchParams, currentTestUser);
696
+ expect(count).toBe(1);
697
+ expect(data).toHaveLength(1);
698
+ expect(data[0]._id.toString()).toBe(docId1.toString());
699
+ await purge();
700
+ });
701
+
702
+ it('devrait trouver un document en utilisant un filtre $find simple avec plusieurs conditions (AND implicite)', async () => {
703
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
704
+ const searchParams = {
705
+ model: comprehensiveTestModelDefinition.name,
706
+ filter: {
707
+ relationSingle: {
708
+ "$find": {
709
+ "relatedName": "Rel_Search_A1",
710
+ "relatedValue": 101
711
+ }
712
+ }
713
+ },
714
+ depth: 1
715
+ };
716
+ const { data, count } = await searchData(searchParams, currentTestUser);
717
+ expect(count).toBe(1);
718
+ expect(data).toHaveLength(1);
719
+ expect(data[0]._id.toString()).toBe(docId1.toString());
720
+ await purge();
721
+ });
722
+
723
+ it('ne devrait pas trouver de document si une des conditions du $find simple échoue', async () => {
724
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
725
+ const searchParams = {
726
+ model: comprehensiveTestModelDefinition.name,
727
+ filter: {
728
+ relationSingle: {
729
+ "$find": {
730
+ "relatedName": "Rel_Search_A1", // Correspond
731
+ "relatedValue": 999 // Ne correspond pas
732
+ }
733
+ }
734
+ },
735
+ depth: 1
736
+ };
737
+ const { data, count } = await searchData(searchParams, currentTestUser);
738
+ expect(count).toBe(0);
739
+ expect(data).toHaveLength(0);
740
+ await purge();
741
+ });
742
+
743
+ it('devrait trouver un document via une relation multiple en utilisant un filtre $find simple', async () => {
744
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
745
+ const searchParams = {
746
+ model: comprehensiveTestModelDefinition.name,
747
+ filter: {
748
+ relationMultiple: {
749
+ "$find": { "relatedValue": 102 } // Devrait trouver docId1 car il a une relation vers relA2
750
+ }
751
+ },
752
+ depth: 1
753
+ };
754
+ const { data, count } = await searchData(searchParams, currentTestUser);
755
+ expect(count).toBe(1);
756
+ expect(data).toHaveLength(1);
757
+ expect(data[0]._id.toString()).toBe(docId1.toString());
758
+ await purge();
759
+ });
760
+
761
+ });
762
+
763
+ describe('installPack', () => {
764
+ let testPacksColInstance;
765
+
766
+ beforeAll(() => {
767
+ // Note: l'initialisation est déjà faite dans le beforeAll global.
768
+ // On s'assure juste d'avoir une référence si on en a besoin spécifiquement ici.
769
+ // Si vous n'avez pas suivi l'étape 1, vous pouvez l'initialiser ici.
770
+ testPacksColInstance = getCollection('packs');
771
+ });
772
+
773
+ beforeEach(async () => {
774
+ // On supprime les données de ce test
775
+ });
776
+
777
+ it('devrait installer un pack, créer les modèles et insérer les données avec relations via $link', async () => {
778
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
779
+ // 1. Définir le pack de données mock
780
+ const mockPack = {
781
+ private: false,
782
+ name: 'test-pack-for-install',
783
+ models: [
784
+ {
785
+ name: 'packUser',
786
+ description: 'Modèle pour les utilisateurs du pack',
787
+ fields: [
788
+ { name: 'username', type: 'string', required: true, unique: true },
789
+ { name: 'level', type: 'number', default: 1 }
790
+ ]
791
+ },
792
+ {
793
+ name: 'packItem',
794
+ description: 'Modèle pour les items du pack',
795
+ fields: [
796
+ { name: 'itemName', type: 'string', required: true },
797
+ { name: 'owner', type: 'relation', relation: 'packUser' }
798
+ ]
799
+ }
800
+ ],
801
+ data: {
802
+ all: { // 'all' pour les données non spécifiques à une langue
803
+ packUser: [
804
+ { username: 'playerOne' },
805
+ { username: 'playerTwo', level: 5 }
806
+ ],
807
+ packItem: [
808
+ // La nouvelle syntaxe $link pour résoudre les relations
809
+ { itemName: 'Sword', owner: { $link: { _model: 'packUser', username: 'playerOne' } } },
810
+ { itemName: 'Shield', owner: { $link: { _model: 'packUser', username: 'playerTwo' } } }
811
+ ]
812
+ }
813
+ }
814
+ };
815
+
816
+ // 2. Insérer le pack dans la collection pour le rendre disponible au test
817
+ const packInsertResult = await testPacksColInstance.insertOne(mockPack);
818
+ const packId = packInsertResult.insertedId.toString();
819
+
820
+ // 3. Appeler la fonction à tester avec la nouvelle signature
821
+ const result = await installPack(packId, currentTestUser, 'en');
822
+
823
+ // 4. Assertions sur le résumé de l'installation
824
+ expect(result.success, `L'installation du pack a échoué: ${result.errors?.join('; ')}`).toBe(true);
825
+ expect(result.summary.models.installed).toHaveLength(2);
826
+ expect(result.summary.datas.inserted).toBe(4); // 2 utilisateurs + 2 items
827
+ expect(result.summary.datas.updated).toBe(2); // 2 items mis à jour avec la relation
828
+
829
+ // 5. Vérifier la création des modèles
830
+ const userModel = await testModelsColInstance.findOne({ name: 'packUser', _user: currentTestUser.username });
831
+ expect(userModel).not.toBeNull();
832
+ const itemModel = await testModelsColInstance.findOne({ name: 'packItem', _user: currentTestUser.username });
833
+ expect(itemModel).not.toBeNull();
834
+ expect(itemModel.fields.find(f => f.name === 'owner').relation).toBe('packUser');
835
+
836
+ // 6. Vérifier l'insertion des données
837
+ const insertedUsers = await testDatasColInstance.find({ _model: 'packUser', _user: currentTestUser.username }).toArray();
838
+ expect(insertedUsers).toHaveLength(2);
839
+ const insertedItems = await testDatasColInstance.find({ _model: 'packItem', _user: currentTestUser.username }).toArray();
840
+ expect(insertedItems).toHaveLength(2);
841
+
842
+ // 7. Vérifier la résolution des relations via $link
843
+ const playerOne = insertedUsers.find(u => u.username === 'playerOne');
844
+ const sword = insertedItems.find(i => i.itemName === 'Sword');
845
+ expect(sword.owner.toString()).toBe(playerOne._id.toString());
846
+
847
+ const playerTwo = insertedUsers.find(u => u.username === 'playerTwo');
848
+ const shield = insertedItems.find(i => i.itemName === 'Shield');
849
+ expect(shield.owner.toString()).toBe(playerTwo._id.toString());
850
+ await purge();
851
+ }, 10000);
852
+
853
+ it("ne devrait installer que les modèles valides d'un pack et rapporter les erreurs", async () => {
854
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
855
+ const mockPackWithInvalidModel = {
856
+ name: 'invalid-model-pack',
857
+ private: false,
858
+ models: [
859
+ {
860
+ // 'name' est requis, on l'omet pour causer une erreur
861
+ description: 'Modèle invalide sans nom',
862
+ fields: []
863
+ },
864
+ { // Un modèle valide pour s'assurer que l'installation continue
865
+ name: 'validPackModel',
866
+ description: 'Un modèle valide',
867
+ fields: [{ name: 'title', type: 'string' }]
868
+ }
869
+ ],
870
+ data: {}
871
+ };
872
+
873
+ const packInsertResult = await testPacksColInstance.insertOne(mockPackWithInvalidModel);
874
+ const packId = packInsertResult.insertedId.toString();
875
+
876
+ const result = await installPack(packId, currentTestUser, 'en');
877
+
878
+ // Le succès global est faux s'il y a des erreurs
879
+ expect(result.success).toBe(false);
880
+ expect(result.errors).toHaveLength(1);
881
+ expect(result.errors[0]).toContain("Failed to install model 'unknown'"); // Le nom est inconnu
882
+
883
+ // Vérifier le résumé de l'option
884
+ expect(result.summary.models.failed).toHaveLength(1);
885
+ expect(result.summary.models.failed[0]).toBe('unknown');
886
+ expect(result.summary.models.installed).toHaveLength(1);
887
+ expect(result.summary.models.installed[0]).toBe('validPackModel');
888
+
889
+ // Vérifier en BDD que le modèle valide a bien été créé
890
+ const validModel = await testModelsColInstance.findOne({ name: 'validPackModel', _user: currentTestUser.username });
891
+ expect(validModel).not.toBeNull();
892
+ await purge();
893
+ });
894
+
895
+ it('devrait correctement insérer des données JSON depuis un pack', async () => {
896
+ const { currentTestUser } = await setupTestContext();
897
+ // 1. Définir le pack avec des données JSON
898
+ const mockPackWithJson = {
899
+ name: 'test-pack-with-json',
900
+ private: false,
901
+ models: [
902
+ {
903
+ name: 'packConfig',
904
+ description: 'Modèle avec un champ JSON',
905
+ fields: [
906
+ { name: 'configName', type: 'string', required: true },
907
+ { name: 'settings', type: 'code', language: 'json' }
908
+ ]
909
+ }
910
+ ],
911
+ data: {
912
+ all: {
913
+ packConfig: [
914
+ {
915
+ configName: 'mainConfig',
916
+ settings: {
917
+ theme: 'dark',
918
+ notifications: {
919
+ enabled: true,
920
+ level: 'important'
921
+ }
922
+ }
923
+ }
924
+ ]
925
+ }
926
+ }
927
+ };
928
+
929
+ // 2. Insérer le pack
930
+ const packInsertResult = await testPacksColInstance.insertOne(mockPackWithJson);
931
+ const packId = packInsertResult.insertedId.toString();
932
+
933
+ // 3. Appeler la fonction à tester
934
+ const result = await installPack(packId, currentTestUser, 'en');
935
+
936
+ // 4. Assertions
937
+ expect(result.success, `L'installation du pack JSON a échoué: ${result.errors?.join('; ')}`).toBe(true);
938
+ expect(result.summary.models.installed).toHaveLength(1);
939
+ expect(result.summary.datas.inserted).toBe(1);
940
+
941
+ // 5. Vérifier les données insérées en BDD
942
+ const datasCol = await getCollectionForUser(currentTestUser);
943
+ const insertedConfig = await datasCol.findOne({ _model: 'packConfig', _user: currentTestUser.username });
944
+
945
+ expect(insertedConfig).not.toBeNull();
946
+ expect(insertedConfig.configName).toBe('mainConfig');
947
+ expect(typeof insertedConfig.settings).toBe('object');
948
+ expect(insertedConfig.settings).toEqual({
949
+ theme: 'dark',
950
+ notifications: {
951
+ enabled: true,
952
+ level: 'important'
953
+ }
954
+ });
955
+
956
+ await getCollection('packs').deleteMany({ _id: packId});
957
+ });
958
+ });
959
+ // In test/data.integration.test.js
960
+
961
+ describe('relationFilter validation', () => {
962
+ let user;
963
+ let activeProductId, inactiveProductId;
964
+
965
+ // Model names are kept in French to match the database, but variables are in English.
966
+ const productModel = {
967
+ name: 'produitTestFiltre',
968
+ description: '',
969
+ fields: [
970
+ { name: 'name', type: 'string' },
971
+ { name: 'actif', type: 'boolean', default: false }
972
+ ]
973
+ };
974
+ const orderModel = {
975
+ name: 'commandeTestFiltre',
976
+ description: '',
977
+ fields: [
978
+ { name: 'ref', type: 'string' },
979
+ {
980
+ name: 'produit',
981
+ type: 'relation',
982
+ relation: 'produitTestFiltre',
983
+ relationFilter: { "$eq":["$actif", true] } // Only link active products
984
+ }
985
+ ]
986
+ };
987
+
988
+ // Set up the context once for all tests in this describe block.
989
+ beforeAll(async () => {
990
+ user = await engine.userProvider.findUserByUsername('demo');
991
+ // Cleanup before starting to ensure a clean state
992
+ await deleteModels({ name: productModel.name, _user: user.username });
993
+ await deleteModels({ name: orderModel.name, _user: user.username });
994
+ await deleteData(productModel.name, {}, user);
995
+ await deleteData(orderModel.name, {}, user);
996
+
997
+ // Create models
998
+ await createModel({ ...productModel, _user: user.username });
999
+ await createModel({ ...orderModel, _user: user.username });
1000
+
1001
+ // Create test data
1002
+ const activeProduct = await insertData(productModel.name, { name: 'Active Product', actif: true }, {}, user);
1003
+ const inactiveProduct = await insertData(productModel.name, { name: 'Inactive Product', actif: false }, {}, user);
1004
+
1005
+ activeProductId = activeProduct.insertedIds[0];
1006
+ inactiveProductId = inactiveProduct.insertedIds[0];
1007
+ });
1008
+
1009
+ // Clean up everything after all tests in this block have run.
1010
+ afterAll(async () => {
1011
+ await deleteModels({ name: productModel.name, _user: user.username });
1012
+ await deleteModels({ name: orderModel.name, _user: user.username });
1013
+ await deleteData(productModel.name, {}, user);
1014
+ await deleteData(orderModel.name, {}, user);
1015
+ const coll = await getCollectionForUser(user);
1016
+ await coll.drop();
1017
+ });
1018
+
1019
+ it('should ALLOW inserting data with a valid relation', async () => {
1020
+ const result = await insertData(orderModel.name, { ref: 'CMD-OK', produit: activeProductId }, {}, user);
1021
+ expect(result.success).toBe(true);
1022
+ expect(result.insertedIds).toHaveLength(1);
1023
+ // Cleanup the created order for test isolation
1024
+ await deleteData(orderModel.name, result.insertedIds, user);
1025
+ });
1026
+
1027
+ it('should REJECT inserting data with a relation that does not respect the filter', async () => {
1028
+ const result = await insertData(orderModel.name, { ref: 'CMD-FAIL', produit: inactiveProductId }, {}, user);
1029
+ expect(result.success).toBe(false);
1030
+ expect(result.error).toContain('produit');
1031
+ });
1032
+
1033
+ it('should ALLOW updating data with a valid relation', async () => {
1034
+ // First, create a valid order
1035
+ const initialOrder = await insertData(orderModel.name, { ref: 'CMD-TO-EDIT', produit: activeProductId }, {}, user);
1036
+
1037
+ // Update it (even with the same value, this tests the code path)
1038
+ const result = await editData(orderModel.name, { _id: initialOrder.insertedIds[0] }, { produit: activeProductId }, {}, user);
1039
+
1040
+ expect(result.success).toBe(true);
1041
+ // The hash might not change if only metadata like _updatedAt changes, so modifiedCount can be 0 or 1.
1042
+ expect(result.modifiedCount).toBeGreaterThanOrEqual(0);
1043
+
1044
+ // Cleanup
1045
+ await deleteData(orderModel.name, initialOrder.insertedIds, user);
1046
+ });
1047
+
1048
+ it('should REJECT updating data with a relation that does not respect the filter', async () => {
1049
+ // First, create a valid order
1050
+ const initialOrder = await insertData(orderModel.name, { ref: 'CMD-TO-EDIT-FAIL', produit: activeProductId }, {}, user);
1051
+
1052
+ // Attempt to link it to an inactive product, expecting it to fail.
1053
+ const result = await editData(orderModel.name, { _id: initialOrder.insertedIds[0] }, { produit: inactiveProductId }, {}, user);
1054
+ expect(result.success).toBe(false);
1055
+ expect(result.error).toContain('produit');
1056
+ // Cleanup
1057
+ await deleteData(orderModel.name, initialOrder.insertedIds, user);
1058
+ });
1059
+ });
1060
+
982
1061
  });