data-primals-engine 1.7.1 → 1.7.2

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 (36) hide show
  1. package/README.md +160 -160
  2. package/client/package-lock.json +484 -175
  3. package/client/package.json +7 -4
  4. package/client/src/AssistantChat.jsx +1 -3
  5. package/client/src/DataLayout.jsx +19 -20
  6. package/client/src/DataTable.jsx +2 -2
  7. package/client/src/DocumentationPageLayout.scss +1 -1
  8. package/client/src/ViewSwitcher.jsx +1 -1
  9. package/client/vite.config.js +31 -30
  10. package/package.json +27 -17
  11. package/src/ai.jobs.js +135 -0
  12. package/src/constants.js +561 -545
  13. package/src/data.js +2 -0
  14. package/src/engine.js +50 -42
  15. package/src/modules/assistant/assistant.js +782 -763
  16. package/src/modules/assistant/constants.js +23 -16
  17. package/src/modules/assistant/providers.js +77 -37
  18. package/src/modules/bucket.js +4 -0
  19. package/src/modules/data/data.cluster.js +191 -0
  20. package/src/modules/data/data.core.js +11 -8
  21. package/src/modules/data/data.js +311 -311
  22. package/src/modules/data/data.operations.js +186 -106
  23. package/src/modules/data/data.relations.js +1 -0
  24. package/src/modules/data/data.replication.js +83 -0
  25. package/src/modules/data/data.routes.js +2183 -1879
  26. package/src/modules/mongodb.js +76 -73
  27. package/src/modules/user.js +7 -1
  28. package/src/modules/worker-script-runner.js +97 -0
  29. package/src/modules/workflow.js +177 -52
  30. package/src/packs.js +5701 -5701
  31. package/src/providers.js +298 -297
  32. package/test/assistant.test.js +207 -206
  33. package/test/data.integration.test.js +1425 -1416
  34. package/test/import_export.integration.test.js +210 -210
  35. package/test/workflow.actions.integration.test.js +487 -475
  36. package/test/workflow.integration.test.js +332 -329
@@ -1,1417 +1,1426 @@
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 fs from 'fs/promises';
5
- import path from 'path';
6
- import os from 'os';
7
- import { Config } from '../src/config.js';
8
- import {
9
- insertData,
10
- editData,
11
- deleteData,
12
- searchData, installPack, deleteModels, createModel, patchData, datasCollection
13
- } from '../src/index.js';
14
-
15
- import {
16
- modelsCollection as getAppModelsCollection,
17
- getCollection,
18
- getCollectionForUser as getAppUserCollection, getCollectionForUser
19
- } from '../src/modules/mongodb.js';
20
- import { isConditionMet } from '../src/filter.js';
21
- import {getRandom, isPlainObject} from "../src/core.js";
22
- import {generateUniqueName, initEngine} from "../src/setenv.js";
23
- import {purgeData} from "../src/modules/data/data.history.js";
24
- import {removeFile} from "../src/modules/file.js";
25
-
26
- let testModelsColInstance;
27
- let testDatasColInstance;
28
-
29
- // Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
30
- async function setupTestContext() {
31
-
32
- const currentTestModelName = generateUniqueName('relatedModel');
33
- const currentRelatedModelName = generateUniqueName('comprehensiveModel');
34
-
35
- // Créer un utilisateur unique pour ce test
36
- const currentTestUser = {
37
- username: generateUniqueName('testuserDataIntegration'),
38
- userPlan: 'premium',
39
- email: generateUniqueName('test') + '@example.com'
40
- };
41
-
42
- // Initialize collection instances after the engine is ready
43
- testModelsColInstance = getAppModelsCollection;
44
- testDatasColInstance = await getAppUserCollection(currentTestUser);
45
-
46
- const relatedModelDefinition = {
47
- name: currentRelatedModelName,
48
- _user: currentTestUser.username,
49
- description: 'Model for testing relations',
50
- fields: [
51
- {name: 'relatedName', type: 'string', required: true, unique: true},
52
- {name: 'relatedValue', type: 'number'}
53
- ],
54
- maxRequestData: 10
55
- };
56
-
57
- const comprehensiveTestModelDefinition = {
58
- name: currentTestModelName,
59
- _user: currentTestUser.username,
60
- description: 'A model with all field types and constraints for testing',
61
- fields: [
62
- // String types
63
- {name: 'stringRequired', type: 'string', required: true, hint: 'A required string'},
64
- {name: 'stringUnique', type: 'string', unique: true, hint: 'A unique string'},
65
- {name: 'stringMaxLength', type: 'string', maxlength: 5, hint: 'String with max length 5'},
66
- {name: 'stringDefault', type: 'string', default: 'defaultString'},
67
- {name: 'string_tLang', type: 'string_t', hint: 'Localizable string'},
68
- {name: 'richtextField', type: 'richtext', maxlength: 200},
69
- {name: 'passwordField', type: 'password'},
70
- {name: 'emailField', type: 'email'},
71
- {name: 'phoneField', type: 'phone'},
72
- {name: 'urlField', type: 'url'},
73
- // Number types
74
- {name: 'number', type: 'number'},
75
- {name: 'numberMinMax', type: 'number', min: 10, max: 20},
76
- {name: 'numberStep', type: 'number', step: 0.5},
77
- {name: 'numberDefault', type: 'number', default: 42},
78
- // Boolean
79
- {name: 'booleanField', type: 'boolean'},
80
- {name: 'booleanDefault', type: 'boolean', default: true},
81
- // Date & Datetime
82
- {name: 'dateField', type: 'date'},
83
- {name: 'datetimeField', type: 'datetime', default: 'now'},
84
- {name: 'dateMinMax', type: 'date', min: '2023-01-01', max: '2023-12-31'},
85
- // Enum
86
- {name: 'enumField', type: 'enum', items: ['alpha', 'beta', 'gamma']},
87
- {name: 'enumDefault', type: 'enum', items: ['one', 'two'], default: 'one'},
88
- // Array
89
- {name: 'arrayString', type: 'array', itemsType: 'string', maxItems: 2},
90
- {name: 'arrayNumber', type: 'array', itemsType: 'number', minItems: 1},
91
- {name: 'arrayEnum', type: 'array', itemsType: 'enum', items: ['a', 'b']},
92
- // Relation
93
- {name: 'relationSingle', type: 'relation', relation: currentRelatedModelName},
94
- {name: 'relationMultiple', type: 'relation', relation: currentRelatedModelName, multiple: true},
95
- // File (metadata validation)
96
- {name: 'fileField', type: 'file', mimeTypes: ['image/png', 'application/pdf', 'text/plain'], maxSize: 1024 * 10}, // 10KB
97
- // Color
98
- {name: 'colorField', type: 'color'},
99
- // Code
100
- {name: 'codeJsonField', type: 'code', language: 'json'},
101
- {name: 'codeJsField', type: 'code', language: 'javascript', maxlength: 100},
102
- // Object (simple validation, structure not deeply validated by default dataTypes.object)
103
- {name: 'objectField', type: 'object'},
104
- // Model & ModelField (validation of string format, not existence)
105
- {name: 'modelNameField', type: 'model'},
106
- {name: 'modelFieldNameField', type: 'modelField'} // Note: modelField type expects an object {model: 'modelName', field: 'fieldName'}
107
- ],
108
- maxRequestData: 50
109
- };
110
-
111
- if( await testModelsColInstance.find({ $or: [{name: comprehensiveTestModelDefinition.name}, {name: relatedModelDefinition.name}]})) {
112
- // Insérer les modèles en base
113
- await testModelsColInstance.insertMany([
114
- comprehensiveTestModelDefinition,
115
- relatedModelDefinition
116
- ]);
117
- }
118
- await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
119
-
120
- const purge = async ()=>{
121
- await purgeData(currentTestUser);
122
- }
123
-
124
- // Retourner toutes les variables nécessaires pour un test
125
- return {
126
- currentTestUser,
127
- comprehensiveTestModelDefinition,
128
- relatedModelDefinition,
129
- purge
130
- };
131
- }
132
-
133
- let engine;
134
- describe('Data integration tests (CRUD, validation...)', () => {
135
-
136
- beforeAll(async () =>{
137
- Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
138
- engine = await initEngine();
139
-
140
- })
141
- describe('insertData avec comprehensiveTestModel', () => {
142
- it('devrait insérer des données valides pour tous les types de champs', async () => {
143
-
144
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
145
- // 1. Créer un suffixe unique pour cette exécution de test
146
- const testSuffix = getRandom(1000, 9999); // Génère un nombre aléatoire
147
-
148
- // 2. Insérer les documents liés avec des noms uniques garantis
149
- const relatedDoc1 = await insertData(relatedModelDefinition.name, {
150
- relatedName: `Rel1_Unique_${testSuffix}`, // Nom unique
151
- relatedValue: 100
152
- }, {}, currentTestUser, false);
153
-
154
- const relatedDoc2 = await insertData(relatedModelDefinition.name, {
155
- relatedName: `Rel2_Unique_${testSuffix}`, // Nom unique
156
- relatedValue: 200
157
- }, {}, currentTestUser, false);
158
-
159
- // S'assurer que les insertions précédentes ont réussi avant de continuer
160
- expect(relatedDoc1.success, "L'insertion du document lié 1 a échoué").toBe(true);
161
- expect(relatedDoc2.success, "L'insertion du document lié 2 a échoué").toBe(true);
162
-
163
- // --- FIN DU SETUP AMÉLIORÉ ---
164
-
165
- const validData = {
166
- stringRequired: 'Valid String',
167
- stringUnique: `UniqueValueForInsert_${testSuffix}`, // Valeur unique garantie
168
- stringMaxLength: '12345',
169
- string_tLang: 'Bonjour',
170
- richtextField: '<p>Valid rich text</p>',
171
- passwordField: 'validPassword123',
172
- emailField: 'valid@example.com',
173
- phoneField: '+1234567890',
174
- urlField: 'https://example.com',
175
- number: 123,
176
- numberMinMax: 15,
177
- numberStep: 10.5,
178
- booleanField: true,
179
- dateField: '2023-07-15',
180
- dateMinMax: '2023-06-01',
181
- enumField: 'alpha',
182
- arrayString: ['one', 'two'],
183
- arrayNumber: [1, 2, 3],
184
- arrayEnum: ['a'],
185
- relationSingle: relatedDoc1.insertedIds[0].toString(),
186
- relationMultiple: [relatedDoc1.insertedIds[0].toString(), relatedDoc2.insertedIds[0].toString()],
187
- // Le mock de fichier est OK, pas besoin de le rendre unique
188
- fileField: { name: 'test.png', type: 'image/png', size: 1024 * 5, guid: `dummy-guid-png-${testSuffix}` },
189
- colorField: '#FF0000',
190
- codeJsonField: { "key": "value" },
191
- codeJsField: 'console.log("hello");',
192
- objectField: { a: 1, b: "test" },
193
- modelNameField: 'someModelName',
194
- modelFieldNameField: { model: 'someModelName', field: 'someFieldName' }
195
- };
196
-
197
- const result = await insertData(comprehensiveTestModelDefinition.name, validData, {}, currentTestUser, false);
198
- expect(result.success, `L'insertion principale a échoué: ${result.error}`).toBe(true);
199
- expect(result.insertedIds).toHaveLength(1);
200
-
201
- console.log(result);
202
- const insertedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(result.insertedIds[0]) });
203
- expect(insertedDoc).not.toBeNull();
204
- expect(insertedDoc.stringRequired).toBe('Valid String');
205
- expect(insertedDoc.stringUnique).toBe(`UniqueValueForInsert_${testSuffix}`); // Vérifier la valeur unique
206
- expect(insertedDoc.stringDefault).toBe('defaultString');
207
- expect(new Date(insertedDoc.datetimeField).getFullYear()).toBe(new Date().getFullYear());
208
- expect(insertedDoc.booleanDefault).toBe(true);
209
- expect(insertedDoc.relationSingle.toString()).toBe(relatedDoc1.insertedIds[0].toString());
210
-
211
- await purge();
212
- }, 10000);
213
-
214
- describe('Validations des champs à l\'insertion', () => {
215
- // --- STRING VALIDATIONS ---
216
- it('devrait rejeter un string requis non fourni', async () => {
217
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
218
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringUnique: 'test' /* stringRequired manquant */ }, {}, currentTestUser, false);
219
- expect(result.success).toBe(false);
220
- expect(result.error).toContain('stringRequired');
221
- await purge();
222
- });
223
-
224
- it('devrait rejeter un string trop long pour maxlength', async () => {
225
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
226
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', stringMaxLength: '123456' }, {}, currentTestUser, false);
227
- expect(result.success).toBe(false);
228
- expect(result.error).toContain('stringMaxLength');
229
- await purge();
230
- });
231
-
232
- it('devrait rejeter une valeur dupliquée pour un champ unique (stringUnique)', async () => {
233
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
234
- await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req1', stringUnique: 'duplicateMe', 'passwordField': 'pass', number: 1, enumField: 'test' }, {}, currentTestUser, false);
235
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req2', stringUnique: 'duplicateMe', 'passwordField': 'pass', number: 1, enumField: 'test' }, {}, currentTestUser, false);
236
- expect(result.success).toBe(false);
237
- await purge();
238
- });
239
-
240
- // --- NUMBER VALIDATIONS ---
241
- it('devrait rejeter un number non numérique', async () => {
242
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
243
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', number: 'not-a-number' }, {}, currentTestUser, false);
244
- expect(result.success).toBe(false);
245
- expect(result.error).toContain('number');
246
- await purge();
247
- });
248
-
249
- it('devrait rejeter un number infur à min (numberMinMax)', async () => {
250
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
251
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', numberMinMax: 5 }, {}, currentTestUser, false);
252
- expect(result.success).toBe(false);
253
- expect(result.error).toContain('numberMinMax');
254
- await purge();
255
- });
256
-
257
- it('devrait rejeter un number supérieur à max (numberMinMax)', async () => {
258
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
259
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', numberMinMax: 25 }, {}, currentTestUser, false);
260
- expect(result.success).toBe(false);
261
- expect(result.error).toContain('numberMinMax');
262
- await purge();
263
- });
264
-
265
- // --- ARRAY VALIDATIONS ---
266
- it('devrait rejeter un array avec trop d\'items (arrayString maxItems: 2)', async () => {
267
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
268
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayString: ['a', 'b', 'c'] }, {}, currentTestUser, false);
269
- expect(result.success).toBe(false);
270
- expect(result.error).toContain('arrayString');
271
- await purge();
272
- });
273
-
274
- it('devrait rejeter un array avec pas assez d\'items (arrayNumber minItems: 1)', async () => {
275
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
276
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayNumber: [] }, {}, currentTestUser, false);
277
- expect(result.success).toBe(false);
278
- expect(result.error).toContain('arrayNumber');
279
- await purge();
280
- });
281
-
282
- it('devrait rejeter un array avec un item de type incorrect (arrayNumber)', async () => {
283
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
284
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayNumber: [1, 'not-a-number', 3] }, {}, currentTestUser, false);
285
- expect(result.success).toBe(false);
286
- expect(result.error).toContain('arrayNumber');
287
- await purge();
288
- });
289
-
290
- // --- FILE VALIDATIONS (metadata) ---
291
- it('devrait rejeter un file avec un mimeType non autorisé', async () => {
292
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
293
- const mockFile = { name: 'test.txt', type: 'text/csv', size: 1024, guid: 'dummy-txt' };
294
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
295
- expect(result.success).toBe(false);
296
- expect(result.error).toContain('image/png'); // Ou un message plus spécifique sur mimeTypes
297
- expect(result.error).toContain('text/plain'); // Ou un message plus spécifique sur mimeTypes
298
- await purge();
299
- });
300
-
301
- it('devrait rejeter un file avec une taille supérieure à maxSize', async () => {
302
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
303
- const mockFile = { name: 'large.png', type: 'image/png', size: 1024 * 15, guid: 'dummy-large' }; // 15KB > 10KB
304
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
305
- expect(result.success).toBe(false);
306
- expect(result.error).toContain('fileField'); // Ou un message plus spécifique sur maxSize
307
- await purge();
308
- });
309
-
310
- // --- EMAIL, URL, PHONE, COLOR (basic format) ---
311
- it('devrait rejeter un email invalide', async () => {
312
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
313
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', emailField: 'not-an-email' }, {}, currentTestUser, false);
314
- expect(result.success).toBe(false);
315
- expect(result.error).toContain('emailField');
316
- await purge();
317
- });
318
- it('devrait rejeter une URL invalide', async () => {
319
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
320
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', urlField: 'not a url' }, {}, currentTestUser, false);
321
- expect(result.success).toBe(false);
322
- expect(result.error).toContain('urlField');
323
- await purge();
324
- });
325
- it('devrait rejeter une couleur invalide', async () => {
326
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
327
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', colorField: '#GGHHII' }, {}, currentTestUser, false);
328
- expect(result.success).toBe(false);
329
- expect(result.error).toContain('colorField');
330
- await purge();
331
- });
332
-
333
- // --- DATE / DATETIME VALIDATIONS ---
334
- it('devrait rejeter une date invalide (format)', async () => {
335
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
336
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', dateField: 'not-a-date' }, {}, currentTestUser, false);
337
- expect(result.success).toBe(false);
338
- expect(result.error).toContain('dateField');
339
- await purge();
340
- });
341
- it('devrait rejeter une date en dehors de min/max (dateMinMax)', async () => {
342
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
343
- const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', dateMinMax: '2024-01-01' }, {}, currentTestUser, false);
344
- expect(result.success).toBe(false);
345
- expect(result.error).toContain('dateMinMax');
346
- await purge();
347
- });
348
-
349
- // TODO: Ajouter des tests pour les autres types et contraintes (password, richtext, code, object, model, modelField)
350
- // Pour 'code' avec language 'json', tester l'insertion d'un JSON invalide si la validation va jusque-là.
351
- // Pour 'relation', tester l'insertion d'un ObjectId non existant (si la validation vérifie l'existence).
352
- });
353
-
354
- // --- NOUVEAU TEST POUR $find ---
355
- it('devrait insérer des données avec des relations référencées par $find', async () => {
356
- // 1. Insérer les documents liés qui seront trouvés par $find
357
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
358
- const relatedDocA = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeA', relatedValue: 111 }, {}, currentTestUser, false);
359
- const relatedDocB = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeB', relatedValue: 222 }, {}, currentTestUser, false);
360
- const relatedDocC = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeC', relatedValue: 222 }, {}, currentTestUser, false);
361
-
362
- const relatedIdA = relatedDocA.insertedIds[0];
363
- const relatedIdB = relatedDocB.insertedIds[0];
364
- const relatedIdC = relatedDocC.insertedIds[0];
365
-
366
- // 2. Prr les données du document principal utilisant $find
367
- const dataWithFindRelations = {
368
- stringRequired: 'DocWithFind',
369
- stringUnique: 'UniqueFindValue',
370
- passwordField: 'findPass',
371
- enumField: 'beta',
372
- number: 99,
373
- // Utiliser $find pour la relation simple
374
- relationSingle: { $find: { $eq: ["$relatedName", 'FindMeA'] } },
375
- // Utiliser un tableau de $find pour la relation multiple
376
- relationMultiple: { $find: { $eq: ["$relatedValue", 222] } },
377
- // Inclure d'autres champs requis ou avec défauts si nécessaire
378
- stringMaxLength: 'short',
379
- emailField: 'find@example.com',
380
- phoneField: '+19876543210',
381
- urlField: 'https://find.example.com',
382
- numberMinMax: 18,
383
- dateField: '2023-11-20',
384
- dateMinMax: '2023-11-20',
385
- arrayNumber: [10],
386
- fileField: { name: 'find.png', type: 'image/png', size: 1024, guid: 'dummy-find-png' },
387
- colorField: '#123456',
388
- codeJsonField: { "find": true },
389
- codeJsField: 'console.log("find");',
390
- objectField: { find: "test" },
391
- modelNameField: 'findModel',
392
- modelFieldNameField: { model: 'findModel', field: 'findField' }
393
- };
394
-
395
- // 3. Insérer le document principal
396
- const result = await insertData(comprehensiveTestModelDefinition.name, dataWithFindRelations, {}, currentTestUser, false);
397
-
398
- // 4. Vérifier le résultat de l'insertion
399
- expect(result.success, `Insertion with $find failed: ${result.error}`).toBe(true);
400
- expect(result.insertedIds).toHaveLength(1);
401
-
402
- // 5. Récupérer le document inséré depuis la base de données
403
- const insertedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(result.insertedIds[0]) });
404
- expect(insertedDoc).not.toBeNull();
405
-
406
- console.log({insertedDoc: insertedDoc, relatedIdB})
407
- expect(insertedDoc.relationSingle).toBe(relatedIdA.toString());
408
-
409
- expect(insertedDoc.relationMultiple).toBeInstanceOf(Array);
410
- expect(insertedDoc.relationMultiple).toHaveLength(2);
411
- // Convertir les ObjectIds en string pour une comparaison facile
412
- const insertedRelationMultipleIds = insertedDoc.relationMultiple.map(id => id.toString());
413
- expect(insertedRelationMultipleIds).toContain(relatedIdB.toString());
414
- expect(insertedRelationMultipleIds).toContain(relatedIdC.toString());
415
-
416
- // Vérifier d'autres champs pour s'assurer que le reste des données est correct
417
- expect(insertedDoc.stringRequired).toBe('DocWithFind');
418
- expect(insertedDoc.enumField).toBe('beta');
419
-
420
- await purge();
421
- }, 10000); // Augmenter le timeout si nécessaire
422
-
423
- it('devrait insérer des données avec un nouveau fichier et le sauvegarder', async () => {
424
- const { currentTestUser, comprehensiveTestModelDefinition, purge } = await setupTestContext();
425
-
426
- // --- Simuler un vrai fichier uploadé comme le ferait formidable/multer ---
427
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'data-primals-test-'));
428
- const filePath = path.join(tmpDir, 'test-upload.txt');
429
- const fileContent = 'Ceci est un vrai test de fichier binaire.';
430
- await fs.writeFile(filePath, fileContent);
431
- const stats = await fs.stat(filePath);
432
-
433
- const uploadedFile = {
434
- filepath: filePath,
435
- originalFilename: 'test-upload-on-insert.txt',
436
- mimetype: 'text/plain',
437
- size: stats.size,
438
- lastModifiedDate: new Date(),
439
- hash: 'test-hash' // Optionnel, mais souvent présent
440
- };
441
- // --- Fin de la simulation ---
442
-
443
- const dataWithFile = {
444
- stringRequired: 'DocWithNewFile',
445
- stringUnique: 'UniqueFileValueForInsert',
446
- passwordField: 'filePass',
447
- enumField: 'gamma',
448
- number: 123
449
- // Le champ 'fileField' est absent ici, il sera fourni dans l'objet 'files'
450
- };
451
-
452
- const result = await insertData(comprehensiveTestModelDefinition.name, dataWithFile, { 'fileField[0]': uploadedFile }, currentTestUser, false);
453
-
454
- expect(result.success, `L'insertion avec un nouveau fichier a échoué: ${result.error}`).toBe(true);
455
- expect(result.insertedIds).toHaveLength(1);
456
-
457
- const query = await searchData( { model: comprehensiveTestModelDefinition.name, filter: { _id: new ObjectId(result.insertedIds[0])}}, currentTestUser);
458
- const insertedDoc = query.data[0];
459
- expect(insertedDoc).not.toBeNull();
460
- console.log(insertedDoc.fileField)
461
- expect(insertedDoc.fileField).toBeInstanceOf(Object);
462
- expect(insertedDoc.fileField.newFile).toBeUndefined(); // La propriété 'newFile' était une hallucination
463
- expect(insertedDoc.fileField._id).toBeDefined();
464
- expect(insertedDoc.fileField.name).toBe('test-upload-on-insert.txt');
465
- expect(insertedDoc.fileField.guid).toBeDefined();
466
-
467
- await removeFile(insertedDoc.fileField.guid, currentTestUser);
468
-
469
- // Nettoyage du répertoire et fichier temporaire
470
- await fs.rm(tmpDir, { recursive: true, force: true });
471
- await purge();
472
- });
473
- });
474
-
475
- describe('editData avec comprehensiveTestModel', async () => {
476
- let docToEditId;
477
- let relatedDocId;
478
-
479
- const initTest = async () => {
480
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
481
- // Insert a base document and a related document to be used in edit tests
482
- const relatedInsert = await insertData(relatedModelDefinition.name, { relatedName: 'RelForEdit', relatedValue: 50 }, {}, currentTestUser, false);
483
- relatedDocId = relatedInsert.insertedIds[0];
484
-
485
- const initialData = {
486
- stringRequired: 'Initial',
487
- stringUnique: 'UniqueForEdit',
488
- enumField: 'alpha',
489
- passwordField: 'initialPass',
490
- relationSingle: relatedDocId
491
- };
492
- const insertResult = await insertData(comprehensiveTestModelDefinition.name, initialData, {}, currentTestUser, false);
493
- docToEditId = insertResult.insertedIds[0];
494
-
495
- return { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge };
496
- };
497
- it('devrait modifier des données valides', async () => {
498
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
499
- const dataToEdit = { stringRequired: 'Edited String', numberMinMax: 12 };
500
- const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, dataToEdit, {}, currentTestUser);
501
-
502
- expect(result.success, `Edit failed: ${result.error}`).toBe(true);
503
- expect(result.modifiedCount).toBe(1);
504
-
505
- const editedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docToEditId) });
506
- expect(editedDoc.stringRequired).toBe('Edited String');
507
- expect(editedDoc.numberMinMax).toBe(12);
508
-
509
- await purge();
510
- });
511
-
512
- it('devrait rejeter une modification avec un string requis vide', async () => {
513
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
514
- const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringRequired: '' }, {}, currentTestUser);
515
- expect(result.success).toBe(false);
516
- expect(result.error).toContain('stringRequired');
517
-
518
- await purge();
519
- });
520
-
521
- it('devrait rejeter une modification avec un number hors des bornes (numberMinMax)', async () => {
522
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
523
- const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringRequired: 'test', numberMinMax: 50 }, {}, currentTestUser);
524
- expect(result.success).toBe(false);
525
- expect(result.error).toContain('numberMinMax');
526
- await purge();
527
- });
528
-
529
- it('devrait rejeter une modification qui viole une contrainte unique (stringUnique)', async ({skip}) => {
530
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
531
- // Insert another doc to create the unique conflict
532
-
533
- await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'Other', stringUnique: 'UniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser, false);
534
- // Insert another doc to create the unique conflict
535
- const res1 = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'Other', stringUnique: 'ExistingUniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser, false);
536
-
537
- const result = await editData(comprehensiveTestModelDefinition.name, { stringUnique: 'ExistingUniqueValue'}, { stringRequired: 'Other', stringUnique: 'UniqueValue', enumField: 'beta', passwordField: 'pass' }, {}, currentTestUser);
538
- expect(result.success).toBe(false);
539
- expect(result.error).toContain('UniqueValue');
540
- expect(result.error).toContain('stringUnique');
541
- await purge();
542
- });
543
-
544
- // TODO: Ajouter plus de tests de validation pour editData, similaires à ceux de insertData
545
- });
546
-
547
- describe('$push operation using patchData', () =>{
548
- describe('$push sur tableau existant', () => {
549
- let docToPatchId;
550
- let setupContext;
551
-
552
- beforeEach(async () => {
553
- setupContext = await setupTestContext();
554
- const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
555
-
556
- const initialData = {
557
- stringRequired: 'InitialForPush',
558
- stringUnique: 'UniqueForPush',
559
- enumField: 'alpha',
560
- passwordField: 'pushPass',
561
- arrayString: ['tag1'],
562
- arrayNumber: [100]
563
- };
564
- const insertResult = await insertData(comprehensiveTestModelDefinition.name, initialData, {}, currentTestUser, false);
565
- docToPatchId = insertResult.insertedIds[0];
566
- });
567
-
568
- afterEach(async () => {
569
- await setupContext.purge();
570
- });
571
-
572
- it('devrait ajouter une seule valeur à un tableau existant', async () => {
573
- const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
574
-
575
- const result = await patchData(comprehensiveTestModelDefinition.name, { _id: docToPatchId }, { arrayString: { $push: 'tag2' } }, {}, currentTestUser);
576
-
577
- expect(result.success, `patchData avec $push a échoué: ${result.error}`).toBe(true);
578
- expect(result.modifiedCount).toBe(1);
579
-
580
- const patchedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docToPatchId) });
581
- expect(patchedDoc.arrayString).toEqual(['tag1', 'tag2']);
582
- });
583
-
584
- it('devrait ajouter plusieurs valeurs à un tableau existant', async () => {
585
- const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
586
-
587
- const result = await patchData(comprehensiveTestModelDefinition.name, { _id: docToPatchId }, { arrayNumber: { $push: [200, 300] } }, {}, currentTestUser);
588
-
589
- expect(result.success, `patchData avec $push multiple a échoué: ${result.error}`).toBe(true);
590
- expect(result.modifiedCount).toBe(1);
591
-
592
- const patchedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docToPatchId) });
593
- expect(patchedDoc.arrayNumber).toEqual([100, 200, 300]);
594
- });
595
-
596
- it('devrait rejeter un push si la valeur est invalide', async () => {
597
- const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
598
-
599
- // Tenter de pusher un nombre dans un tableau de strings
600
- const result = await patchData(comprehensiveTestModelDefinition.name, { _id: docToPatchId }, { arrayString: { $push: 12345 } }, {}, currentTestUser);
601
-
602
- expect(result.success).toBe(false);
603
- expect(result.error).toContain("Valeur invalide fournie pour le tableau 'arrayString'");
604
- });
605
-
606
- it('devrait rejeter un push si une des valeurs du tableau est invalide', async () => {
607
- const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
608
-
609
- // Tenter de pusher un tableau contenant une valeur invalide
610
- const result = await patchData(comprehensiveTestModelDefinition.name, { _id: docToPatchId }, { arrayNumber: { $push: [400, 'not-a-number', 500] } }, {}, currentTestUser);
611
-
612
- expect(result.success).toBe(false);
613
- expect(result.error).toContain("Valeur invalide fournie pour le tableau 'arrayNumber'");
614
- });
615
-
616
- });
617
- describe('$push sur un tableau inexistant', () => {
618
- it('devrait créer le tableau et pusher les valeurs s\'il n\'existe pas', async () => {
619
- // On utilise un setup de contexte complètement isolé pour ce test
620
- const { currentTestUser, comprehensiveTestModelDefinition, purge } = await setupTestContext();
621
-
622
- const initialData = { stringRequired: 'NoArrayField', stringUnique: 'UniqueForNoArrayPush', enumField: 'beta', passwordField: 'pass' };
623
- const insertResult = await insertData(comprehensiveTestModelDefinition.name, initialData, {}, currentTestUser, false);
624
- const newDocId = insertResult.insertedIds[0];
625
-
626
- const result = await patchData(comprehensiveTestModelDefinition.name, { _id: newDocId }, { arrayString: { $push: ['first', 'second'] } }, {}, currentTestUser);
627
-
628
- expect(result.success, `patchData sur un tableau inexistant a échoué: ${result.error}`).toBe(true);
629
- expect(result.modifiedCount).toBe(1);
630
-
631
- const patchedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(newDocId) });
632
- expect(patchedDoc.arrayString).toEqual(['first', 'second']);
633
- await purge();
634
- });
635
- });
636
- });
637
-
638
- // --- Tests deleteData (repris de votre exemple, peuvent être gardés tels quels ou adaptés) ---
639
- describe('deleteData (tests existants)', () => {
640
- it('supprimer des données par ID (deleteData)', async () => {
641
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
642
- const dataToDelete = { name: 'Objet à supprimer', value: 300 };
643
- // Utiliser le modèle simple pour ces tests delete pour ne pas avoir à remplir tous les champs requis de comprehensive
644
- const simpleModel = { name: 'simpleDeleteModel', _user: currentTestUser.username, description: '', fields: [{ name: 'name', type: 'string' }, { name: 'value', type: 'number' }] };
645
- await testModelsColInstance.insertOne(simpleModel);
646
-
647
- const insertResult = await insertData(simpleModel.name, dataToDelete, {}, currentTestUser, false);
648
- const docId = insertResult.insertedIds[0];
649
-
650
- const deleteResult = await deleteData(simpleModel.name, [docId], currentTestUser);
651
-
652
- expect(deleteResult.success).toBe(true);
653
- expect(deleteResult.deletedCount).toBe(1);
654
-
655
- const deletedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docId) });
656
- expect(deletedDoc).toBeNull();
657
- await purge();
658
- await purge();
659
- });
660
-
661
- it('supprimer des données par filtre (deleteData)', async () => {
662
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
663
- const simpleModel = { name: 'deleteModelTest', _user: currentTestUser.username, description: '',fields: [{ name: 'name', type: 'string' }, { name: 'value', type: 'number' }] };
664
- await testModelsColInstance.insertOne(simpleModel);
665
-
666
- await insertData(simpleModel.name, { name: 'Item Filtrable 1', value: 401 }, {}, currentTestUser, false);
667
- await insertData(simpleModel.name, { name: 'Item Filtrable 2', value: 402 }, {}, currentTestUser, false);
668
- await insertData(simpleModel.name, { name: 'Autre Item', value: 403 }, {}, currentTestUser, false);
669
-
670
- const filter = { $regexMatch: {
671
- input: '$name',
672
- regex: "^Item Filtrable"
673
- } };
674
- const deleteResult = await deleteData(simpleModel.name, filter, currentTestUser);
675
-
676
- expect(deleteResult.success).toBe(true);
677
- expect(deleteResult.deletedCount).toBe(2);
678
-
679
- const remainingCount = await testDatasColInstance.countDocuments({ _user: currentTestUser.username, _model: simpleModel.name, name: { $regex: "^Item Filtrable" } });
680
- expect(remainingCount).toBe(0);
681
- const totalRemaining = await testDatasColInstance.countDocuments({ _user: currentTestUser.username, _model: simpleModel.name });
682
- expect(totalRemaining).toBe(1);
683
- await purge();
684
- });
685
- });
686
-
687
- describe('searchData avec comprehensiveTestModel et filtres $find', async () => {
688
- let docId1, docId2;
689
- let relatedDocId_A1, relatedDocId_A2, relatedDocId_B1;
690
-
691
- const initTest =async () => {
692
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
693
-
694
- // Insérer des documents liés
695
- const relA1 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_A1', relatedValue: 101 }, {}, currentTestUser, false);
696
- relatedDocId_A1 = relA1.insertedIds[0].toString();
697
- const relA2 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_A2', relatedValue: 102 }, {}, currentTestUser, false);
698
- relatedDocId_A2 = relA2.insertedIds[0].toString();
699
- const relB1 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_B1', relatedValue: 201 }, {}, currentTestUser, false);
700
- relatedDocId_B1 = relB1.insertedIds[0].toString();
701
-
702
- // Insérer des documents principaux
703
- const data1 = {
704
- stringRequired: 'SearchDoc1',
705
- stringUnique: 'UniqueSearch1',
706
- enumField: 'alpha',
707
- passwordField: 'searchPass1',
708
- relationSingle: relatedDocId_A1,
709
- relationMultiple: [relatedDocId_A1, relatedDocId_A2],
710
- number: 10
711
- };
712
- const insertResult1 = await insertData(comprehensiveTestModelDefinition.name, data1, {}, currentTestUser, false);
713
- docId1 = insertResult1.insertedIds[0].toString();
714
-
715
- const data2 = {
716
- stringRequired: 'SearchDoc2',
717
- stringUnique: 'UniqueSearch2',
718
- enumField: 'beta',
719
- passwordField: 'searchPass2',
720
- relationSingle: relatedDocId_B1,
721
- relationMultiple: [relatedDocId_B1],
722
- number: 20
723
- };
724
- const insertResult2 = await insertData(comprehensiveTestModelDefinition.name, data2, {}, currentTestUser, false);
725
- docId2 = insertResult2.insertedIds[0].toString();
726
-
727
- return { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge };
728
- };
729
-
730
- it('devrait trouver un document par une condition sur une relation simple ($find)', async () => {
731
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
732
- const searchParams = {
733
- model: comprehensiveTestModelDefinition.name,
734
- filter: {
735
- relationSingle: {
736
- "$find": { "$and": [{"$eq": ["$$this.relatedValue", 101]}] }
737
- }
738
- },
739
- autoExpand: true,
740
- depth: 8
741
- };
742
- const { data, count } = await searchData(searchParams, currentTestUser);
743
- expect(count).toBe(1);
744
- expect(data).toHaveLength(1);
745
- expect(data[0]._id.toString()).toBe(docId1.toString());
746
- // Vérifier que la relation est bien populée si depth > 0
747
- expect(data[0].relationSingle).toBeInstanceOf(Object);
748
- expect(data[0].relationSingle.relatedName).toBe('Rel_Search_A1');
749
- await purge();
750
- });
751
-
752
- it('ne devrait pas trouver de document si la condition $find sur relation simple ne correspond pas', async () => {
753
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
754
- const searchParams = {
755
- model: comprehensiveTestModelDefinition.name,
756
- filter: {
757
- relationSingle: {
758
- "$find": { "$eq": ["$$this.relatedName", "NonExistentRelName"] }
759
- }
760
- },
761
- depth: 1
762
- };
763
- const { data, count } = await searchData(searchParams, currentTestUser);
764
- expect(count).toBe(0);
765
- expect(data).toHaveLength(0);
766
- await purge();
767
- });
768
-
769
- it('devrait trouver des documents par une condition sur une relation multiple ($find)', async () => {
770
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
771
- const searchParams = {
772
- model: comprehensiveTestModelDefinition.name,
773
- filter: {
774
- $and: [{$ne:["$stringRequired", null]}, {
775
- relationMultiple: { // Le champ qui est un tableau de relations
776
- "$find": {"$eq": ["$$this.relatedValue", 102]} // Condition sur les documents liés
777
- }
778
- }]
779
- },
780
- depth: 1
781
- };
782
- const { data, count } = await searchData(searchParams, currentTestUser);
783
- expect(count).toBe(1);
784
- expect(data).toHaveLength(1);
785
- expect(data[0]._id.toString()).toBe(docId1.toString());
786
- // Note: searchData with depth > 0 will populate the relationMultiple array
787
- expect(data[0].relationMultiple).toBeInstanceOf(Array);
788
- expect(data[0].relationMultiple.some(rel => rel.relatedValue === 102)).toBe(true);
789
- await purge();
790
- });
791
-
792
-
793
- it('devrait trouver des documents en combinant un filtre normal et un filtre $find', async () => {
794
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
795
- const searchParams = {
796
- model: comprehensiveTestModelDefinition.name,
797
- filter: {
798
- stringRequired: "SearchDoc1", // Filtre sur le modèle principal
799
- relationSingle: {
800
- "$find": {"$gt": ["$$this.relatedValue", 100] } // Filtre sur la relation
801
- }
802
- },
803
- depth: 1
804
- };
805
- const { data, count } = await searchData(searchParams,currentTestUser);
806
- expect(count).toBe(1);
807
- expect(data).toHaveLength(1);
808
- expect(data[0]._id.toString()).toBe(docId1.toString());
809
- await purge();
810
- });
811
-
812
- it('devrait retourner un tableau vide si $find ne correspond à aucun document lié', async () => {
813
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
814
- const searchParams = {
815
- model: comprehensiveTestModelDefinition.name,
816
- filter: {
817
- relationMultiple: {
818
- "$find": { "$eq": ["$$this.relatedName", "NonExistentRelNameForMultiple"] }
819
- }
820
- },
821
- depth: 1
822
- };
823
- const { data, count } = await searchData(searchParams,currentTestUser);
824
- expect(count).toBe(0);
825
- expect(data).toHaveLength(0);
826
- await purge();
827
- });
828
-
829
- // --- NOUVEAUX TESTS POUR LE SHORTHAND $find ---
830
-
831
- it('devrait trouver un document en utilisant un filtre $find simple (shorthand)', async () => {
832
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
833
- const searchParams = {
834
- model: comprehensiveTestModelDefinition.name,
835
- filter: {
836
- relationSingle: {
837
- "$find": { "relatedValue": 101 } // Utilisation du shorthand { champ: valeur }
838
- }
839
- },
840
- depth: 1
841
- };
842
- const { data, count } = await searchData(searchParams, currentTestUser);
843
- expect(count).toBe(1);
844
- expect(data).toHaveLength(1);
845
- expect(data[0]._id.toString()).toBe(docId1.toString());
846
- await purge();
847
- });
848
-
849
- it('devrait trouver un document en utilisant un filtre $find simple avec plusieurs conditions (AND implicite)', async () => {
850
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
851
- const searchParams = {
852
- model: comprehensiveTestModelDefinition.name,
853
- filter: {
854
- relationSingle: {
855
- "$find": {
856
- "relatedName": "Rel_Search_A1",
857
- "relatedValue": 101
858
- }
859
- }
860
- },
861
- depth: 1
862
- };
863
- const { data, count } = await searchData(searchParams, currentTestUser);
864
- expect(count).toBe(1);
865
- expect(data).toHaveLength(1);
866
- expect(data[0]._id.toString()).toBe(docId1.toString());
867
- await purge();
868
- });
869
-
870
- it('ne devrait pas trouver de document si une des conditions du $find simple échoue', async () => {
871
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
872
- const searchParams = {
873
- model: comprehensiveTestModelDefinition.name,
874
- filter: {
875
- relationSingle: {
876
- "$find": {
877
- "relatedName": "Rel_Search_A1", // Correspond
878
- "relatedValue": 999 // Ne correspond pas
879
- }
880
- }
881
- },
882
- depth: 1
883
- };
884
- const { data, count } = await searchData(searchParams, currentTestUser);
885
- expect(count).toBe(0);
886
- expect(data).toHaveLength(0);
887
- await purge();
888
- });
889
-
890
- it('devrait trouver un document via une relation multiple en utilisant un filtre $find simple', async () => {
891
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
892
- const searchParams = {
893
- model: comprehensiveTestModelDefinition.name,
894
- filter: {
895
- relationMultiple: {
896
- "$find": { "relatedValue": 102 } // Devrait trouver docId1 car il a une relation vers relA2
897
- }
898
- },
899
- depth: 1
900
- };
901
- const { data, count } = await searchData(searchParams, currentTestUser);
902
- expect(count).toBe(1);
903
- expect(data).toHaveLength(1);
904
- expect(data[0]._id.toString()).toBe(docId1.toString());
905
- await purge();
906
- });
907
-
908
- });
909
-
910
- describe('installPack', () => {
911
- let testPacksColInstance;
912
-
913
- beforeAll(() => {
914
- // Note: l'initialisation est déjà faite dans le beforeAll global.
915
- // On s'assure juste d'avoir une référence si on en a besoin spécifiquement ici.
916
- // Si vous n'avez pas suivi l'étape 1, vous pouvez l'initialiser ici.
917
- testPacksColInstance = getCollection('packs');
918
- });
919
-
920
- beforeEach(async () => {
921
- // On supprime les données de ce test
922
- });
923
-
924
- it('devrait installer un pack, créer les modèles et insérer les données avec relations via $link', async () => {
925
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
926
- // 1. Définir le pack de données mock
927
- const mockPack = {
928
- private: false,
929
- name: 'test-pack-for-install',
930
- models: [
931
- {
932
- name: 'packUser',
933
- description: 'Modèle pour les utilisateurs du pack',
934
- fields: [
935
- { name: 'username', type: 'string', required: true, unique: true },
936
- { name: 'level', type: 'number', default: 1 }
937
- ]
938
- },
939
- {
940
- name: 'packItem',
941
- description: 'Modèle pour les items du pack',
942
- fields: [
943
- { name: 'itemName', type: 'string', required: true },
944
- { name: 'owner', type: 'relation', relation: 'packUser' }
945
- ]
946
- }
947
- ],
948
- data: {
949
- all: { // 'all' pour les données non spécifiques à une langue
950
- packUser: [
951
- { username: 'playerOne' },
952
- { username: 'playerTwo', level: 5 }
953
- ],
954
- packItem: [
955
- // La nouvelle syntaxe $link pour résoudre les relations
956
- { itemName: 'Sword', owner: { $link: { _model: 'packUser', username: 'playerOne' } } },
957
- { itemName: 'Shield', owner: { $link: { _model: 'packUser', username: 'playerTwo' } } }
958
- ]
959
- }
960
- }
961
- };
962
-
963
- // 2. Insérer le pack dans la collection pour le rendre disponible au test
964
- const packInsertResult = await testPacksColInstance.insertOne(mockPack);
965
- const packId = packInsertResult.insertedId.toString();
966
-
967
- // 3. Appeler la fonction à tester avec la nouvelle signature
968
- const result = await installPack(packId, currentTestUser, 'en');
969
-
970
- // 4. Assertions sur le résumé de l'installation
971
- expect(result.success, `L'installation du pack a échoué: ${result.errors?.join('; ')}`).toBe(true);
972
- expect(result.summary.models.installed).toHaveLength(2);
973
- expect(result.summary.datas.inserted).toBe(4); // 2 utilisateurs + 2 items
974
- expect(result.summary.datas.updated).toBe(2); // 2 items mis à jour avec la relation
975
-
976
- // 5. Vérifier la création des modèles
977
- const userModel = await testModelsColInstance.findOne({ name: 'packUser', _user: currentTestUser.username });
978
- expect(userModel).not.toBeNull();
979
- const itemModel = await testModelsColInstance.findOne({ name: 'packItem', _user: currentTestUser.username });
980
- expect(itemModel).not.toBeNull();
981
- expect(itemModel.fields.find(f => f.name === 'owner').relation).toBe('packUser');
982
-
983
- // 6. Vérifier l'insertion des données
984
- const insertedUsers = await testDatasColInstance.find({ _model: 'packUser', _user: currentTestUser.username }).toArray();
985
- expect(insertedUsers).toHaveLength(2);
986
- const insertedItems = await testDatasColInstance.find({ _model: 'packItem', _user: currentTestUser.username }).toArray();
987
- expect(insertedItems).toHaveLength(2);
988
-
989
- // 7. Vérifier la résolution des relations via $link
990
- const playerOne = insertedUsers.find(u => u.username === 'playerOne');
991
- const sword = insertedItems.find(i => i.itemName === 'Sword');
992
- expect(sword.owner.toString()).toBe(playerOne._id.toString());
993
-
994
- const playerTwo = insertedUsers.find(u => u.username === 'playerTwo');
995
- const shield = insertedItems.find(i => i.itemName === 'Shield');
996
- expect(shield.owner.toString()).toBe(playerTwo._id.toString());
997
- await purge();
998
- }, 10000);
999
-
1000
- it("ne devrait installer que les modèles valides d'un pack et rapporter les erreurs", async () => {
1001
- const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
1002
- const mockPackWithInvalidModel = {
1003
- name: 'invalid-model-pack',
1004
- private: false,
1005
- models: [
1006
- {
1007
- // 'name' est requis, on l'omet pour causer une erreur
1008
- description: 'Modèle invalide sans nom',
1009
- fields: []
1010
- },
1011
- { // Un modèle valide pour s'assurer que l'installation continue
1012
- name: 'validPackModel',
1013
- description: 'Un modèle valide',
1014
- fields: [{ name: 'title', type: 'string' }]
1015
- }
1016
- ],
1017
- data: {}
1018
- };
1019
-
1020
- const packInsertResult = await testPacksColInstance.insertOne(mockPackWithInvalidModel);
1021
- const packId = packInsertResult.insertedId.toString();
1022
-
1023
- const result = await installPack(packId, currentTestUser, 'en');
1024
-
1025
- // Le succès global est faux s'il y a des erreurs
1026
- expect(result.success).toBe(false);
1027
- expect(result.errors).toHaveLength(1);
1028
- expect(result.errors[0]).toContain("Failed to install model 'unknown'"); // Le nom est inconnu
1029
-
1030
- // Vérifier le résumé de l'option
1031
- expect(result.summary.models.failed).toHaveLength(1);
1032
- expect(result.summary.models.failed[0]).toBe('unknown');
1033
- expect(result.summary.models.installed).toHaveLength(1);
1034
- expect(result.summary.models.installed[0]).toBe('validPackModel');
1035
-
1036
- // Vérifier en BDD que le modèle valide a bien été créé
1037
- const validModel = await testModelsColInstance.findOne({ name: 'validPackModel', _user: currentTestUser.username });
1038
- expect(validModel).not.toBeNull();
1039
- await purge();
1040
- });
1041
-
1042
- it('devrait correctement insérer des données JSON depuis un pack', async () => {
1043
- const { currentTestUser } = await setupTestContext();
1044
- // 1. Définir le pack avec des données JSON
1045
- const mockPackWithJson = {
1046
- name: 'test-pack-with-json',
1047
- private: false,
1048
- models: [
1049
- {
1050
- name: 'packConfig',
1051
- description: 'Modèle avec un champ JSON',
1052
- fields: [
1053
- { name: 'configName', type: 'string', required: true },
1054
- { name: 'settings', type: 'code', language: 'json' }
1055
- ]
1056
- }
1057
- ],
1058
- data: {
1059
- all: {
1060
- packConfig: [
1061
- {
1062
- configName: 'mainConfig',
1063
- settings: {
1064
- theme: 'dark',
1065
- notifications: {
1066
- enabled: true,
1067
- level: 'important'
1068
- }
1069
- }
1070
- }
1071
- ]
1072
- }
1073
- }
1074
- };
1075
-
1076
- // 2. Insérer le pack
1077
- const packInsertResult = await testPacksColInstance.insertOne(mockPackWithJson);
1078
- const packId = packInsertResult.insertedId.toString();
1079
-
1080
- // 3. Appeler la fonction à tester
1081
- const result = await installPack(packId, currentTestUser, 'en');
1082
-
1083
- // 4. Assertions
1084
- expect(result.success, `L'installation du pack JSON a échoué: ${result.errors?.join('; ')}`).toBe(true);
1085
- expect(result.summary.models.installed).toHaveLength(1);
1086
- expect(result.summary.datas.inserted).toBe(1);
1087
-
1088
- // 5. Vérifier les données insérées en BDD
1089
- const datasCol = await getCollectionForUser(currentTestUser);
1090
- const insertedConfig = await datasCol.findOne({ _model: 'packConfig', _user: currentTestUser.username });
1091
-
1092
- expect(insertedConfig).not.toBeNull();
1093
- expect(insertedConfig.configName).toBe('mainConfig');
1094
- expect(typeof insertedConfig.settings).toBe('object');
1095
- expect(insertedConfig.settings).toEqual({
1096
- theme: 'dark',
1097
- notifications: {
1098
- enabled: true,
1099
- level: 'important'
1100
- }
1101
- });
1102
-
1103
- await getCollection('packs').deleteMany({ _id: packId});
1104
- });
1105
- });
1106
- // In test/data.integration.test.js
1107
-
1108
- describe('relationFilter validation', () => {
1109
- let user;
1110
- let activeProductId, inactiveProductId;
1111
-
1112
- // Model names are kept in French to match the database, but variables are in English.
1113
- const productModel = {
1114
- name: 'produitTestFiltre',
1115
- description: '',
1116
- fields: [
1117
- { name: 'name', type: 'string' },
1118
- { name: 'actif', type: 'boolean', default: false }
1119
- ]
1120
- };
1121
- const orderModel = {
1122
- name: 'commandeTestFiltre',
1123
- description: '',
1124
- fields: [
1125
- { name: 'ref', type: 'string' },
1126
- {
1127
- name: 'produit',
1128
- type: 'relation',
1129
- relation: 'produitTestFiltre',
1130
- relationFilter: { "$eq":["$actif", true] } // Only link active products
1131
- }
1132
- ]
1133
- };
1134
-
1135
- // Set up the context once for all tests in this describe block.
1136
- beforeAll(async () => {
1137
- user = await engine.userProvider.findUserByUsername('demo');
1138
- // Cleanup before starting to ensure a clean state
1139
- await deleteModels({ name: productModel.name, _user: user.username });
1140
- await deleteModels({ name: orderModel.name, _user: user.username });
1141
- await deleteData(productModel.name, {}, user);
1142
- await deleteData(orderModel.name, {}, user);
1143
-
1144
- // Create models
1145
- await createModel({ ...productModel, _user: user.username });
1146
- await createModel({ ...orderModel, _user: user.username });
1147
-
1148
- // Create test data
1149
- const activeProduct = await insertData(productModel.name, { name: 'Active Product', actif: true }, {}, user);
1150
- const inactiveProduct = await insertData(productModel.name, { name: 'Inactive Product', actif: false }, {}, user);
1151
-
1152
- activeProductId = activeProduct.insertedIds[0];
1153
- inactiveProductId = inactiveProduct.insertedIds[0];
1154
- });
1155
-
1156
- // Clean up everything after all tests in this block have run.
1157
- afterAll(async () => {
1158
- await deleteModels({ name: productModel.name, _user: user.username });
1159
- await deleteModels({ name: orderModel.name, _user: user.username });
1160
- await deleteData(productModel.name, {}, user);
1161
- await deleteData(orderModel.name, {}, user);
1162
- const coll = await getCollectionForUser(user);
1163
- await coll.drop();
1164
- });
1165
-
1166
- it('should ALLOW inserting data with a valid relation', async () => {
1167
- const result = await insertData(orderModel.name, { ref: 'CMD-OK', produit: activeProductId }, {}, user);
1168
- expect(result.success).toBe(true);
1169
- expect(result.insertedIds).toHaveLength(1);
1170
- // Cleanup the created order for test isolation
1171
- await deleteData(orderModel.name, result.insertedIds, user);
1172
- });
1173
-
1174
- it('should REJECT inserting data with a relation that does not respect the filter', async () => {
1175
- const result = await insertData(orderModel.name, { ref: 'CMD-FAIL', produit: inactiveProductId }, {}, user);
1176
- expect(result.success).toBe(false);
1177
- expect(result.error).toContain('produit');
1178
- });
1179
-
1180
- it('should ALLOW updating data with a valid relation', async () => {
1181
- // First, create a valid order
1182
- const initialOrder = await insertData(orderModel.name, { ref: 'CMD-TO-EDIT', produit: activeProductId }, {}, user);
1183
-
1184
- // Update it (even with the same value, this tests the code path)
1185
- const result = await editData(orderModel.name, { _id: initialOrder.insertedIds[0] }, { produit: activeProductId }, {}, user);
1186
-
1187
- expect(result.success).toBe(true);
1188
- // The hash might not change if only metadata like _updatedAt changes, so modifiedCount can be 0 or 1.
1189
- expect(result.modifiedCount).toBeGreaterThanOrEqual(0);
1190
-
1191
- // Cleanup
1192
- await deleteData(orderModel.name, initialOrder.insertedIds, user);
1193
- });
1194
-
1195
- it('should REJECT updating data with a relation that does not respect the filter', async () => {
1196
- // First, create a valid order
1197
- const initialOrder = await insertData(orderModel.name, { ref: 'CMD-TO-EDIT-FAIL', produit: activeProductId }, {}, user);
1198
-
1199
- // Attempt to link it to an inactive product, expecting it to fail.
1200
- const result = await editData(orderModel.name, { _id: initialOrder.insertedIds[0] }, { produit: inactiveProductId }, {}, user);
1201
- expect(result.success).toBe(false);
1202
- expect(result.error).toContain('produit');
1203
- // Cleanup
1204
- await deleteData(orderModel.name, initialOrder.insertedIds, user);
1205
- });
1206
- });
1207
-
1208
- describe('searchData avec filtre _env', () => {
1209
- let user, testModel, noEnvDocId;
1210
-
1211
- beforeAll(async () => {
1212
- // Setup
1213
- const context = await setupTestContext();
1214
- user = context.currentTestUser;
1215
- testModel = {
1216
- name: generateUniqueName('envTestModel'),
1217
- description: '',
1218
- _user: user.username,
1219
- fields: [{ name: 'title', type: 'string' }]
1220
- };
1221
- await createModel(testModel);
1222
-
1223
- // Insert test data
1224
- await insertData(testModel.name, { title: 'Prod Data', _env: 'prod' }, {}, user, false);
1225
- await insertData(testModel.name, { title: 'Staging Data', _env: 'staging' }, {}, user, false);
1226
- const noEnvResult = await insertData(testModel.name, { title: 'No Env Data' }, {}, user, false);
1227
- noEnvDocId = noEnvResult.insertedIds[0];
1228
- });
1229
-
1230
- afterAll(async () => {
1231
- // Cleanup
1232
- await deleteModels({ name: testModel.name, _user: user.username });
1233
- await deleteData(testModel.name, {}, user);
1234
- });
1235
-
1236
- it('devrait trouver uniquement les données avec _env="prod"', async () => {
1237
- const { data, count } = await searchData({
1238
- model: testModel.name,
1239
- env: 'prod'
1240
- }, user);
1241
- expect(count).toBe(1);
1242
- expect(data).toHaveLength(1);
1243
- expect(data[0].title).toBe('Prod Data');
1244
- });
1245
-
1246
- it('devrait trouver uniquement les données avec _env="staging"', async () => {
1247
- const { data, count } = await searchData({
1248
- model: testModel.name,
1249
- env: 'staging'
1250
- }, user);
1251
- expect(count).toBe(1);
1252
- expect(data).toHaveLength(1);
1253
- expect(data[0].title).toBe('Staging Data');
1254
- });
1255
-
1256
- it('devrait trouver uniquement les données SANS _env spécifié', async () => {
1257
- const { data, count } = await searchData({
1258
- model: testModel.name
1259
- // _env est omis intentionnellement
1260
- }, user);
1261
- expect(count).toBe(1);
1262
- expect(data).toHaveLength(1);
1263
- expect(data[0].title).toBe('No Env Data');
1264
- });
1265
-
1266
- it('ne devrait trouver aucune donnée pour un _env inexistant', async () => {
1267
- const { data, count } = await searchData({
1268
- model: testModel.name,
1269
- env: 'nonexistent'
1270
- }, user);
1271
- expect(count).toBe(0);
1272
- expect(data).toHaveLength(0);
1273
- });
1274
-
1275
- it('devrait permettre de modifier le champ _env via editData', async () => {
1276
- const result = await editData(testModel.name, { _id: noEnvDocId }, { _env: 'newly_assigned' }, {}, user);
1277
- expect(result.success).toBe(true);
1278
- const { data, count } = await searchData({ model: testModel.name, env: 'newly_assigned' }, user);
1279
- expect(count).toBe(1);
1280
- expect(data[0]._id.toString()).toBe(noEnvDocId.toString());
1281
- });
1282
- });
1283
-
1284
- describe('Data operations with permission filters', () => {
1285
- let testUser, testSystemUser, permissionAdd, permissionEdit, permissionDelete, role;
1286
- let orderModelDef, pendingOrderId, shippedOrderId, otherUserOrderId;
1287
-
1288
- beforeEach(async () => {
1289
- // Création d'un utilisateur de test isolé
1290
- const username = generateUniqueName('perm_filter_user');
1291
- testUser = { _id: new ObjectId(), username: username, _user: 'testSystemUser', _model: 'user', roles: [] };
1292
- testSystemUser = { _id: new ObjectId(), username: 'testSystemUser', roles: ['API_ADMIN'] }; // L'utilisateur système a tous les droits
1293
- const collection = await getCollectionForUser(testUser);
1294
-
1295
- // 1. Créer les modèles nécessaires
1296
- await createModel({ name: 'permission', description:'t', _user: testSystemUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'filter', type: 'code', language: 'json' }, { name: 'description', type: 'string' }] });
1297
- await createModel({ name: 'role',description:'t', _user: testSystemUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'permissions', type: 'relation', relation: 'permission', multiple: true }] });
1298
- // Le modèle 'orderTest' doit exister pour les deux utilisateurs : testUser (pour les opérations de test) et testSystemUser (pour le setup)
1299
- orderModelDef = { name: 'orderTest',description:'t', _user: testSystemUser.username, fields: [{ name: 'status', type: 'string' }, { name: 'amount', type: 'number' }, { name: 'customer', type: 'string' }] };
1300
- const t = await createModel(orderModelDef);
1301
-
1302
- // 2. Créer les permissions avec filtres pour les tests
1303
- const permissionsToCreate = [
1304
- // On a besoin de ces permissions pour que testUser puisse créer le rôle et les permissions
1305
- { name: 'API_ADD_DATA_permission', _user: testSystemUser.username },
1306
- { name: 'API_ADD_DATA_role', _user: testSystemUser.username },
1307
- // Permissions de l'application
1308
- { name: 'API_SEARCH_DATA_orderTest' }, // <-- AJOUTER CETTE LIGNE
1309
- { name: 'API_ADD_DATA_orderTest', filter: { "$lt" : ["$amount", 1000] }, description: "Filter for adding orders" },
1310
- { name: 'API_EDIT_DATA_orderTest', filter: { status: 'pending' } },
1311
- { name: 'API_DELETE_DATA_orderTest', filter: { customer: testUser._id.toString() } }
1312
- ];
1313
- // On insère les permissions pour les deux utilisateurs pour que le rôle soit valide pour testUser
1314
- let permResult = await insertData('permission', permissionsToCreate, {}, testSystemUser, false, false, false); // Pour le setup
1315
- expect(permResult.success, `La création des permissions a échoué: ${permResult.error}`).toBe(true);
1316
-
1317
- // 3. Récupérer les IDs des permissions et créer le rôle
1318
- // On se base sur les permissions de l'utilisateur de test pour créer son rôle
1319
- const allPerms = await (await getCollectionForUser(testSystemUser)).find({ _model: 'permission' }).toArray();
1320
- const permIds = allPerms.map(p => p._id.toString());
1321
-
1322
- // On insère le rôle pour les deux utilisateurs
1323
- let roleRes = await insertData('role', { name: 'OrderManager', permissions: permIds }, {}, testSystemUser, false, false, false);
1324
- expect(roleRes.success, `La création du rôle a échoué: ${roleRes.error}`).toBe(true);
1325
- role = roleRes.insertedIds[0];
1326
-
1327
- // 4. Assigner le rôle à l'utilisateur de test
1328
- testUser.roles = [role.toString()];
1329
-
1330
- // 5. Créer les données de test avec l'utilisateur de test lui-même.
1331
- // Pour cela, on lui donne temporairement les droits d'admin pour le setup.
1332
- const originalRoles = testUser.roles;
1333
- testUser.roles = ['API_ADMIN']; // Droits temporaires pour créer les données
1334
- const pendingOrder = await insertData('orderTest', { status: 'pending', amount: 100, customer: testUser._id.toString() }, {}, testUser, false, false, false);
1335
- pendingOrderId = pendingOrder.insertedIds[0];
1336
- const shippedOrder = await insertData('orderTest', { status: 'shipped', amount: 200, customer: testUser._id.toString() }, {}, testUser, false, false, false);
1337
- shippedOrderId = shippedOrder.insertedIds[0];
1338
- const otherOrder = await insertData('orderTest', { status: 'pending', amount: 300, customer: 'other_user_id' }, {}, testUser, false, false, false);
1339
- otherUserOrderId = otherOrder.insertedIds[0];
1340
- testUser.roles = originalRoles; // Restaurer les vrais rôles pour le test
1341
- });
1342
-
1343
- afterEach(async () => {
1344
- await (await getCollectionForUser(testUser)).deleteMany({ _user: testUser.username });
1345
- await (await getCollectionForUser(testSystemUser)).deleteMany({ _user: testSystemUser.username });
1346
- await deleteModels({ _user: testUser.username });
1347
- await deleteModels({ _user: testSystemUser.username });
1348
- });
1349
-
1350
- afterAll(async () =>{
1351
- await purgeData(testUser);
1352
- await purgeData(testSystemUser);
1353
- await datasCollection.drop();
1354
- })
1355
-
1356
- // --- TEST POUR insertData ---
1357
- it('should REJECT inserting data that violates the permission filter', async () => {
1358
- // This should fail because the amount (1500) is not < 1000
1359
- const result = await insertData('orderTest', { status: 'new', amount: 1500, customer: 'new_customer' }, {}, testUser, false, false);
1360
-
1361
- expect(result.success).toBe(false);
1362
-
1363
- });
1364
-
1365
- it('should ALLOW inserting data that respects the permission filter', async () => {
1366
- // This should succeed because the amount (500) is < 1000
1367
- const result = await insertData('orderTest', { status: 'new', amount: 500, customer: 'new_customer' }, {}, testUser, false, false);
1368
-
1369
- expect(result.success, `L'insertion aurait dû réussir: ${result.error}`).toBe(true);
1370
- expect(result.insertedIds).toHaveLength(1);
1371
- });
1372
-
1373
- // --- TESTS POUR editData ---
1374
- it('should ALLOW editing a document that matches the permission filter', async () => {
1375
- // Le filtre de permission est { status: 'pending' }
1376
- const result = await editData('orderTest', { _id: pendingOrderId }, { amount: 150 }, {}, testUser, false, false);
1377
-
1378
- expect(result.success, `editData failed with error: ${result.error}`).toBe(true);
1379
- expect(result.modifiedCount).toBeGreaterThanOrEqual(0); // Peut être 0 si le hash ne change pas
1380
-
1381
- const coll = await getCollectionForUser(testUser);
1382
- const doc = await coll.findOne({ _id: new ObjectId(pendingOrderId)});
1383
- expect(doc.amount).toBe(150);
1384
- });
1385
-
1386
- it('should REJECT editing a document that does NOT match the permission filter', async () => {
1387
- // On tente de modifier la commande 'shipped', mais la permission ne l'autorise que pour 'pending'
1388
- const result = await editData('orderTest', { $eq: ['$_id', new ObjectId(shippedOrderId)] }, { amount: 250 }, {}, testUser, false, false);
1389
-
1390
- expect(result.success).toBe(false);
1391
- });
1392
-
1393
- // --- TESTS POUR deleteData ---
1394
- it('should ALLOW deleting a document that matches the permission filter', async () => {
1395
- // La permission autorise la suppression des commandes `customer` est l'ID de l'utilisateur
1396
- const result = await deleteData('orderTest', { _id: new ObjectId(pendingOrderId) }, testUser, false, false);
1397
-
1398
- expect(result.success, `deleteData failed with error: ${result.error}`).toBe(true);
1399
- expect(result.deletedCount).toBe(1);
1400
-
1401
- const doc = await (await getCollectionForUser(testUser)).findOne({ _id: new ObjectId(pendingOrderId) });
1402
- expect(doc).toBeNull();
1403
- });
1404
-
1405
- it('should REJECT deleting a document that does NOT match the permission filter', async () => {
1406
- // On tente de supprimer une commande qui n'appartient pas à l'utilisateur
1407
- const result = await deleteData('orderTest', { _id: otherUserOrderId }, testUser, false, false);
1408
-
1409
- // La fonction va trouver 0 document à supprimer après application du filtre de permission
1410
- expect(result.success).toBe(true); // La fonction réussit, mais ne supprime rien
1411
- expect(result.deletedCount).toBe(0);
1412
-
1413
- const doc = await (await getCollectionForUser(testSystemUser)).findOne({ _id: new ObjectId(otherUserOrderId) });
1414
- expect(doc).not.toBeNull(); // Le document existe toujours
1415
- });
1416
- });
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 fs from 'fs/promises';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import { Config } from '../src/config.js';
8
+ import {
9
+ insertData,
10
+ editData,
11
+ deleteData,
12
+ searchData, installPack, deleteModels, createModel, patchData, datasCollection
13
+ } from '../src/index.js';
14
+
15
+ import {
16
+ modelsCollection as getAppModelsCollection,
17
+ getCollection,
18
+ getCollectionForUser as getAppUserCollection, getCollectionForUser
19
+ } from '../src/modules/mongodb.js';
20
+ import { isConditionMet } from '../src/filter.js';
21
+ import {getRandom, isPlainObject} from "../src/core.js";
22
+ import {generateUniqueName, initEngine} from "../src/setenv.js";
23
+ import {purgeData} from "../src/modules/data/data.history.js";
24
+ import {removeFile} from "../src/modules/file.js";
25
+
26
+ let testModelsColInstance;
27
+ let testDatasColInstance;
28
+
29
+ // Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
30
+ async function setupTestContext() {
31
+
32
+ const currentTestModelName = generateUniqueName('relatedModel');
33
+ const currentRelatedModelName = generateUniqueName('comprehensiveModel');
34
+
35
+ // Créer un utilisateur unique pour ce test
36
+ const currentTestUser = {
37
+ username: generateUniqueName('testuserDataIntegration'),
38
+ userPlan: 'premium',
39
+ email: generateUniqueName('test') + '@example.com'
40
+ };
41
+
42
+ // Initialize collection instances after the engine is ready
43
+ testModelsColInstance = getAppModelsCollection;
44
+ testDatasColInstance = await getAppUserCollection(currentTestUser);
45
+
46
+ const relatedModelDefinition = {
47
+ name: currentRelatedModelName,
48
+ _user: currentTestUser.username,
49
+ description: 'Model for testing relations',
50
+ fields: [
51
+ {name: 'relatedName', type: 'string', required: true, unique: true},
52
+ {name: 'relatedValue', type: 'number'}
53
+ ],
54
+ maxRequestData: 10
55
+ };
56
+
57
+ const comprehensiveTestModelDefinition = {
58
+ name: currentTestModelName,
59
+ _user: currentTestUser.username,
60
+ description: 'A model with all field types and constraints for testing',
61
+ fields: [
62
+ // String types
63
+ {name: 'stringRequired', type: 'string', required: true, hint: 'A required string'},
64
+ {name: 'stringUnique', type: 'string', unique: true, hint: 'A unique string'},
65
+ {name: 'stringMaxLength', type: 'string', maxlength: 5, hint: 'String with max length 5'},
66
+ {name: 'stringDefault', type: 'string', default: 'defaultString'},
67
+ {name: 'string_tLang', type: 'string_t', hint: 'Localizable string'},
68
+ {name: 'richtextField', type: 'richtext', maxlength: 200},
69
+ {name: 'passwordField', type: 'password'},
70
+ {name: 'emailField', type: 'email'},
71
+ {name: 'phoneField', type: 'phone'},
72
+ {name: 'urlField', type: 'url'},
73
+ // Number types
74
+ {name: 'number', type: 'number'},
75
+ {name: 'numberMinMax', type: 'number', min: 10, max: 20},
76
+ {name: 'numberStep', type: 'number', step: 0.5},
77
+ {name: 'numberDefault', type: 'number', default: 42},
78
+ // Boolean
79
+ {name: 'booleanField', type: 'boolean'},
80
+ {name: 'booleanDefault', type: 'boolean', default: true},
81
+ // Date & Datetime
82
+ {name: 'dateField', type: 'date'},
83
+ {name: 'datetimeField', type: 'datetime', default: 'now'},
84
+ {name: 'dateMinMax', type: 'date', min: '2023-01-01', max: '2023-12-31'},
85
+ // Enum
86
+ {name: 'enumField', type: 'enum', items: ['alpha', 'beta', 'gamma']},
87
+ {name: 'enumDefault', type: 'enum', items: ['one', 'two'], default: 'one'},
88
+ // Array
89
+ {name: 'arrayString', type: 'array', itemsType: 'string', maxItems: 2},
90
+ {name: 'arrayNumber', type: 'array', itemsType: 'number', minItems: 1},
91
+ {name: 'arrayEnum', type: 'array', itemsType: 'enum', items: ['a', 'b']},
92
+ // Relation
93
+ {name: 'relationSingle', type: 'relation', relation: currentRelatedModelName},
94
+ {name: 'relationMultiple', type: 'relation', relation: currentRelatedModelName, multiple: true},
95
+ // File (metadata validation)
96
+ {name: 'fileField', type: 'file', mimeTypes: ['image/png', 'application/pdf', 'text/plain'], maxSize: 1024 * 10}, // 10KB
97
+ // Color
98
+ {name: 'colorField', type: 'color'},
99
+ // Code
100
+ {name: 'codeJsonField', type: 'code', language: 'json'},
101
+ {name: 'codeJsField', type: 'code', language: 'javascript', maxlength: 100},
102
+ // Object (simple validation, structure not deeply validated by default dataTypes.object)
103
+ {name: 'objectField', type: 'object'},
104
+ // Model & ModelField (validation of string format, not existence)
105
+ {name: 'modelNameField', type: 'model'},
106
+ {name: 'modelFieldNameField', type: 'modelField'} // Note: modelField type expects an object {model: 'modelName', field: 'fieldName'}
107
+ ],
108
+ maxRequestData: 50
109
+ };
110
+
111
+ if( await testModelsColInstance.find({ $or: [{name: comprehensiveTestModelDefinition.name}, {name: relatedModelDefinition.name}]})) {
112
+ // Insérer les modèles en base
113
+ await testModelsColInstance.insertMany([
114
+ comprehensiveTestModelDefinition,
115
+ relatedModelDefinition
116
+ ]);
117
+ }
118
+ await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
119
+
120
+ const purge = async ()=>{
121
+ await purgeData(currentTestUser);
122
+ }
123
+
124
+ // Retourner toutes les variables nécessaires pour un test
125
+ return {
126
+ currentTestUser,
127
+ comprehensiveTestModelDefinition,
128
+ relatedModelDefinition,
129
+ purge
130
+ };
131
+ }
132
+
133
+ let engine;
134
+ describe('Data integration tests (CRUD, validation...)', () => {
135
+
136
+ beforeAll(async () =>{
137
+ Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
138
+ engine = await initEngine();
139
+
140
+ })
141
+ describe('insertData avec comprehensiveTestModel', () => {
142
+ it('devrait insérer des données valides pour tous les types de champs', async () => {
143
+
144
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
145
+ // 1. Créer un suffixe unique pour cette exécution de test
146
+ const testSuffix = getRandom(1000, 9999); // Génère un nombre aléatoire
147
+
148
+ // 2. Insérer les documents liés avec des noms uniques garantis
149
+ const relatedDoc1 = await insertData(relatedModelDefinition.name, {
150
+ relatedName: `Rel1_Unique_${testSuffix}`, // Nom unique
151
+ relatedValue: 100
152
+ }, {}, currentTestUser, false);
153
+
154
+ const relatedDoc2 = await insertData(relatedModelDefinition.name, {
155
+ relatedName: `Rel2_Unique_${testSuffix}`, // Nom unique
156
+ relatedValue: 200
157
+ }, {}, currentTestUser, false);
158
+
159
+ // S'assurer que les insertions précédentes ont réussi avant de continuer
160
+ expect(relatedDoc1.success, "L'insertion du document lié 1 a échoué").toBe(true);
161
+ expect(relatedDoc2.success, "L'insertion du document lié 2 a échoué").toBe(true);
162
+
163
+ // --- FIN DU SETUP AMÉLIORÉ ---
164
+
165
+ const validData = {
166
+ stringRequired: 'Valid String',
167
+ stringUnique: `UniqueValueForInsert_${testSuffix}`, // Valeur unique garantie
168
+ stringMaxLength: '12345',
169
+ string_tLang: 'Bonjour',
170
+ richtextField: '<p>Valid rich text</p>',
171
+ passwordField: 'validPassword123',
172
+ emailField: 'valid@example.com',
173
+ phoneField: '+1234567890',
174
+ urlField: 'https://example.com',
175
+ number: 123,
176
+ numberMinMax: 15,
177
+ numberStep: 10.5,
178
+ booleanField: true,
179
+ dateField: '2023-07-15',
180
+ dateMinMax: '2023-06-01',
181
+ enumField: 'alpha',
182
+ arrayString: ['one', 'two'],
183
+ arrayNumber: [1, 2, 3],
184
+ arrayEnum: ['a'],
185
+ relationSingle: relatedDoc1.insertedIds[0].toString(),
186
+ relationMultiple: [relatedDoc1.insertedIds[0].toString(), relatedDoc2.insertedIds[0].toString()],
187
+ // Le mock de fichier est OK, pas besoin de le rendre unique
188
+ fileField: { name: 'test.png', type: 'image/png', size: 1024 * 5, guid: `dummy-guid-png-${testSuffix}` },
189
+ colorField: '#FF0000',
190
+ codeJsonField: { "key": "value" },
191
+ codeJsField: 'console.log("hello");',
192
+ objectField: { a: 1, b: "test" },
193
+ modelNameField: 'someModelName',
194
+ modelFieldNameField: { model: 'someModelName', field: 'someFieldName' }
195
+ };
196
+
197
+ const result = await insertData(comprehensiveTestModelDefinition.name, validData, {}, currentTestUser, false);
198
+ expect(result.success, `L'insertion principale a échoué: ${result.error}`).toBe(true);
199
+ expect(result.insertedIds).toHaveLength(1);
200
+
201
+ console.log(result);
202
+ const insertedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(result.insertedIds[0]) });
203
+ expect(insertedDoc).not.toBeNull();
204
+ expect(insertedDoc.stringRequired).toBe('Valid String');
205
+ expect(insertedDoc.stringUnique).toBe(`UniqueValueForInsert_${testSuffix}`); // Vérifier la valeur unique
206
+ expect(insertedDoc.stringDefault).toBe('defaultString');
207
+ expect(new Date(insertedDoc.datetimeField).getFullYear()).toBe(new Date().getFullYear());
208
+ expect(insertedDoc.booleanDefault).toBe(true);
209
+ expect(insertedDoc.relationSingle.toString()).toBe(relatedDoc1.insertedIds[0].toString());
210
+
211
+ await purge();
212
+ }, 10000);
213
+
214
+ describe('Validations des champs à l\'insertion', () => {
215
+ // --- STRING VALIDATIONS ---
216
+ it('devrait rejeter un string requis non fourni', async () => {
217
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
218
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringUnique: 'test' /* stringRequired manquant */ }, {}, currentTestUser, false);
219
+ expect(result.success).toBe(false);
220
+ expect(result.error).toContain('stringRequired');
221
+ await purge();
222
+ });
223
+
224
+ it('devrait rejeter un string trop long pour maxlength', async () => {
225
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
226
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', stringMaxLength: '123456' }, {}, currentTestUser, false);
227
+ expect(result.success).toBe(false);
228
+ expect(result.error).toContain('stringMaxLength');
229
+ await purge();
230
+ });
231
+
232
+ it('devrait rejeter une valeur dupliquée pour un champ unique (stringUnique)', async () => {
233
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
234
+ await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req1', stringUnique: 'duplicateMe', 'passwordField': 'pass', number: 1, enumField: 'test' }, {}, currentTestUser, false);
235
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req2', stringUnique: 'duplicateMe', 'passwordField': 'pass', number: 1, enumField: 'test' }, {}, currentTestUser, false);
236
+ expect(result.success).toBe(false);
237
+ await purge();
238
+ });
239
+
240
+ // --- NUMBER VALIDATIONS ---
241
+ it('devrait rejeter un number non numérique', async () => {
242
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
243
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', number: 'not-a-number' }, {}, currentTestUser, false);
244
+ expect(result.success).toBe(false);
245
+ expect(result.error).toContain('number');
246
+ await purge();
247
+ });
248
+
249
+ it('devrait rejeter un number infur à min (numberMinMax)', async () => {
250
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
251
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', numberMinMax: 5 }, {}, currentTestUser, false);
252
+ expect(result.success).toBe(false);
253
+ expect(result.error).toContain('numberMinMax');
254
+ await purge();
255
+ });
256
+
257
+ it('devrait rejeter un number supérieur à max (numberMinMax)', async () => {
258
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
259
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', numberMinMax: 25 }, {}, currentTestUser, false);
260
+ expect(result.success).toBe(false);
261
+ expect(result.error).toContain('numberMinMax');
262
+ await purge();
263
+ });
264
+
265
+ // --- ARRAY VALIDATIONS ---
266
+ it('devrait rejeter un array avec trop d\'items (arrayString maxItems: 2)', async () => {
267
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
268
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayString: ['a', 'b', 'c'] }, {}, currentTestUser, false);
269
+ expect(result.success).toBe(false);
270
+ expect(result.error).toContain('arrayString');
271
+ await purge();
272
+ });
273
+
274
+ it('devrait rejeter un array avec pas assez d\'items (arrayNumber minItems: 1)', async () => {
275
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
276
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayNumber: [] }, {}, currentTestUser, false);
277
+ expect(result.success).toBe(false);
278
+ expect(result.error).toContain('arrayNumber');
279
+ await purge();
280
+ });
281
+
282
+ it('devrait rejeter un array avec un item de type incorrect (arrayNumber)', async () => {
283
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
284
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', arrayNumber: [1, 'not-a-number', 3] }, {}, currentTestUser, false);
285
+ expect(result.success).toBe(false);
286
+ expect(result.error).toContain('arrayNumber');
287
+ await purge();
288
+ });
289
+
290
+ // --- FILE VALIDATIONS (metadata) ---
291
+ it('devrait rejeter un file avec un mimeType non autorisé', async () => {
292
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
293
+ const mockFile = { name: 'test.txt', type: 'text/csv', size: 1024, guid: 'dummy-txt' };
294
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
295
+ expect(result.success).toBe(false);
296
+ expect(result.error).toContain('image/png'); // Ou un message plus spécifique sur mimeTypes
297
+ expect(result.error).toContain('text/plain'); // Ou un message plus spécifique sur mimeTypes
298
+ await purge();
299
+ });
300
+
301
+ it('devrait rejeter un file avec une taille supérieure à maxSize', async () => {
302
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
303
+ const mockFile = { name: 'large.png', type: 'image/png', size: 1024 * 15, guid: 'dummy-large' }; // 15KB > 10KB
304
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', fileField: mockFile }, {}, currentTestUser, false);
305
+ expect(result.success).toBe(false);
306
+ expect(result.error).toContain('fileField'); // Ou un message plus spécifique sur maxSize
307
+ await purge();
308
+ });
309
+
310
+ // --- EMAIL, URL, PHONE, COLOR (basic format) ---
311
+ it('devrait rejeter un email invalide', async () => {
312
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
313
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', emailField: 'not-an-email' }, {}, currentTestUser, false);
314
+ expect(result.success).toBe(false);
315
+ expect(result.error).toContain('emailField');
316
+ await purge();
317
+ });
318
+ it('devrait rejeter une URL invalide', async () => {
319
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
320
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', urlField: 'not a url' }, {}, currentTestUser, false);
321
+ expect(result.success).toBe(false);
322
+ expect(result.error).toContain('urlField');
323
+ await purge();
324
+ });
325
+ it('devrait rejeter une couleur invalide', async () => {
326
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
327
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', colorField: '#GGHHII' }, {}, currentTestUser, false);
328
+ expect(result.success).toBe(false);
329
+ expect(result.error).toContain('colorField');
330
+ await purge();
331
+ });
332
+
333
+ // --- DATE / DATETIME VALIDATIONS ---
334
+ it('devrait rejeter une date invalide (format)', async () => {
335
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
336
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', dateField: 'not-a-date' }, {}, currentTestUser, false);
337
+ expect(result.success).toBe(false);
338
+ expect(result.error).toContain('dateField');
339
+ await purge();
340
+ });
341
+ it('devrait rejeter une date en dehors de min/max (dateMinMax)', async () => {
342
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
343
+ const result = await insertData(comprehensiveTestModelDefinition.name, { stringRequired: 'req', dateMinMax: '2024-01-01' }, {}, currentTestUser, false);
344
+ expect(result.success).toBe(false);
345
+ expect(result.error).toContain('dateMinMax');
346
+ await purge();
347
+ });
348
+
349
+ // TODO: Ajouter des tests pour les autres types et contraintes (password, richtext, code, object, model, modelField)
350
+ // Pour 'code' avec language 'json', tester l'insertion d'un JSON invalide si la validation va jusque-là.
351
+ // Pour 'relation', tester l'insertion d'un ObjectId non existant (si la validation vérifie l'existence).
352
+ });
353
+
354
+ // --- NOUVEAU TEST POUR $find ---
355
+ it('devrait insérer des données avec des relations référencées par $find', async () => {
356
+ // 1. Insérer les documents liés qui seront trouvés par $find
357
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
358
+ const relatedDocA = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeA', relatedValue: 111 }, {}, currentTestUser, false);
359
+ const relatedDocB = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeB', relatedValue: 222 }, {}, currentTestUser, false);
360
+ const relatedDocC = await insertData(relatedModelDefinition.name, { relatedName: 'FindMeC', relatedValue: 222 }, {}, currentTestUser, false);
361
+
362
+ const relatedIdA = relatedDocA.insertedIds[0];
363
+ const relatedIdB = relatedDocB.insertedIds[0];
364
+ const relatedIdC = relatedDocC.insertedIds[0];
365
+
366
+ // 2. Prr les données du document principal utilisant $find
367
+ const dataWithFindRelations = {
368
+ stringRequired: 'DocWithFind',
369
+ stringUnique: 'UniqueFindValue',
370
+ passwordField: 'findPass',
371
+ enumField: 'beta',
372
+ number: 99,
373
+ // Utiliser $find pour la relation simple
374
+ relationSingle: { $find: { $eq: ["$relatedName", 'FindMeA'] } },
375
+ // Utiliser un tableau de $find pour la relation multiple
376
+ relationMultiple: { $find: { $eq: ["$relatedValue", 222] } },
377
+ // Inclure d'autres champs requis ou avec défauts si nécessaire
378
+ stringMaxLength: 'short',
379
+ emailField: 'find@example.com',
380
+ phoneField: '+19876543210',
381
+ urlField: 'https://find.example.com',
382
+ numberMinMax: 18,
383
+ dateField: '2023-11-20',
384
+ dateMinMax: '2023-11-20',
385
+ arrayNumber: [10],
386
+ fileField: { name: 'find.png', type: 'image/png', size: 1024, guid: 'dummy-find-png' },
387
+ colorField: '#123456',
388
+ codeJsonField: { "find": true },
389
+ codeJsField: 'console.log("find");',
390
+ objectField: { find: "test" },
391
+ modelNameField: 'findModel',
392
+ modelFieldNameField: { model: 'findModel', field: 'findField' }
393
+ };
394
+
395
+ // 3. Insérer le document principal
396
+ const result = await insertData(comprehensiveTestModelDefinition.name, dataWithFindRelations, {}, currentTestUser, false);
397
+
398
+ // 4. Vérifier le résultat de l'insertion
399
+ expect(result.success, `Insertion with $find failed: ${result.error}`).toBe(true);
400
+ expect(result.insertedIds).toHaveLength(1);
401
+
402
+ // 5. Récupérer le document inséré depuis la base de données
403
+ const insertedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(result.insertedIds[0]) });
404
+ expect(insertedDoc).not.toBeNull();
405
+
406
+ console.log({insertedDoc: insertedDoc, relatedIdB})
407
+ expect(insertedDoc.relationSingle).toBe(relatedIdA.toString());
408
+
409
+ expect(insertedDoc.relationMultiple).toBeInstanceOf(Array);
410
+ expect(insertedDoc.relationMultiple).toHaveLength(2);
411
+ // Convertir les ObjectIds en string pour une comparaison facile
412
+ const insertedRelationMultipleIds = insertedDoc.relationMultiple.map(id => id.toString());
413
+ expect(insertedRelationMultipleIds).toContain(relatedIdB.toString());
414
+ expect(insertedRelationMultipleIds).toContain(relatedIdC.toString());
415
+
416
+ // Vérifier d'autres champs pour s'assurer que le reste des données est correct
417
+ expect(insertedDoc.stringRequired).toBe('DocWithFind');
418
+ expect(insertedDoc.enumField).toBe('beta');
419
+
420
+ await purge();
421
+ }, 10000); // Augmenter le timeout si nécessaire
422
+
423
+ it('devrait insérer des données avec un nouveau fichier et le sauvegarder', async () => {
424
+ const { currentTestUser, comprehensiveTestModelDefinition, purge } = await setupTestContext();
425
+
426
+ // --- Simuler un vrai fichier uploadé comme le ferait formidable/multer ---
427
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'data-primals-test-'));
428
+ const filePath = path.join(tmpDir, 'test-upload.txt');
429
+ const fileContent = 'Ceci est un vrai test de fichier binaire.';
430
+ await fs.writeFile(filePath, fileContent);
431
+ const stats = await fs.stat(filePath);
432
+
433
+ const uploadedFile = {
434
+ filepath: filePath,
435
+ originalFilename: 'test-upload-on-insert.txt',
436
+ mimetype: 'text/plain',
437
+ size: stats.size,
438
+ lastModifiedDate: new Date(),
439
+ hash: 'test-hash' // Optionnel, mais souvent présent
440
+ };
441
+ // --- Fin de la simulation ---
442
+
443
+ const dataWithFile = {
444
+ stringRequired: 'DocWithNewFile',
445
+ stringUnique: 'UniqueFileValueForInsert',
446
+ passwordField: 'filePass',
447
+ enumField: 'gamma',
448
+ number: 123
449
+ // Le champ 'fileField' est absent ici, il sera fourni dans l'objet 'files'
450
+ };
451
+
452
+ const result = await insertData(comprehensiveTestModelDefinition.name, dataWithFile, { 'fileField[0]': uploadedFile }, currentTestUser, false);
453
+
454
+ expect(result.success, `L'insertion avec un nouveau fichier a échoué: ${result.error}`).toBe(true);
455
+ expect(result.insertedIds).toHaveLength(1);
456
+
457
+ const query = await searchData( { model: comprehensiveTestModelDefinition.name, filter: { _id: new ObjectId(result.insertedIds[0])}}, currentTestUser);
458
+ const insertedDoc = query.data[0];
459
+ expect(insertedDoc).not.toBeNull();
460
+ console.log(insertedDoc.fileField)
461
+ expect(insertedDoc.fileField).toBeInstanceOf(Object);
462
+ expect(insertedDoc.fileField.newFile).toBeUndefined(); // La propriété 'newFile' était une hallucination
463
+ expect(insertedDoc.fileField._id).toBeDefined();
464
+ expect(insertedDoc.fileField.name).toBe('test-upload-on-insert.txt');
465
+ expect(insertedDoc.fileField.guid).toBeDefined();
466
+
467
+ await removeFile(insertedDoc.fileField.guid, currentTestUser);
468
+
469
+ // Nettoyage du répertoire et fichier temporaire
470
+ await fs.rm(tmpDir, { recursive: true, force: true });
471
+ await purge();
472
+ });
473
+ });
474
+
475
+ describe('editData avec comprehensiveTestModel', async () => {
476
+ let docToEditId;
477
+ let relatedDocId;
478
+
479
+ const initTest = async () => {
480
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
481
+ // Insert a base document and a related document to be used in edit tests
482
+ const relatedInsert = await insertData(relatedModelDefinition.name, { relatedName: 'RelForEdit', relatedValue: 50 }, {}, currentTestUser, false);
483
+ relatedDocId = relatedInsert.insertedIds[0];
484
+
485
+ const initialData = {
486
+ stringRequired: 'Initial',
487
+ stringUnique: 'UniqueForEdit',
488
+ enumField: 'alpha',
489
+ passwordField: 'initialPass',
490
+ relationSingle: relatedDocId
491
+ };
492
+ const insertResult = await insertData(comprehensiveTestModelDefinition.name, initialData, {}, currentTestUser, false);
493
+ docToEditId = insertResult.insertedIds[0];
494
+
495
+ return { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge };
496
+ };
497
+ it('devrait modifier des données valides', async () => {
498
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
499
+ const dataToEdit = { stringRequired: 'Edited String', numberMinMax: 12 };
500
+ const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, dataToEdit, {}, currentTestUser);
501
+
502
+ expect(result.success, `Edit failed: ${result.error}`).toBe(true);
503
+ expect(result.modifiedCount).toBe(1);
504
+
505
+ const editedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docToEditId) });
506
+ expect(editedDoc.stringRequired).toBe('Edited String');
507
+ expect(editedDoc.numberMinMax).toBe(12);
508
+
509
+ await purge();
510
+ });
511
+
512
+ it('devrait rejeter une modification avec un string requis vide', async () => {
513
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
514
+ const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringRequired: '' }, {}, currentTestUser);
515
+ expect(result.success).toBe(false);
516
+ expect(result.error).toContain('stringRequired');
517
+
518
+ await purge();
519
+ });
520
+
521
+ it('devrait rejeter une modification avec un number hors des bornes (numberMinMax)', async () => {
522
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
523
+ const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringRequired: 'test', numberMinMax: 50 }, {}, currentTestUser);
524
+ expect(result.success).toBe(false);
525
+ expect(result.error).toContain('numberMinMax');
526
+ await purge();
527
+ });
528
+
529
+ it('devrait rejeter une modification qui viole une contrainte unique (stringUnique)', async () => {
530
+ const { currentTestUser, comprehensiveTestModelDefinition, purge } = await initTest();
531
+
532
+ // 1. Insérer un second document avec une valeur unique différente
533
+ const anotherDocData = {
534
+ stringRequired: 'Another Doc',
535
+ stringUnique: 'AnotherUniqueValue',
536
+ passwordField: 'anotherPass',
537
+ enumField: 'beta',
538
+ number: 10
539
+ };
540
+ const anotherInsertResult = await insertData(comprehensiveTestModelDefinition.name, anotherDocData, {}, currentTestUser, false);
541
+ expect(anotherInsertResult.success, "L'insertion du second document pour le test a échoué").toBe(true);
542
+
543
+ // 2. Tenter de mettre à jour le premier document pour avoir la même valeur unique que le second
544
+ const result = await editData(comprehensiveTestModelDefinition.name, docToEditId, { stringUnique: 'AnotherUniqueValue' }, {}, currentTestUser);
545
+
546
+ // 3. Vérifier que l'opération a échoué avec le bon message d'erreur
547
+ expect(result.success).toBe(false);
548
+ expect(result.error).toContain('stringUnique'); // Le nom du champ qui a échoué
549
+
550
+ await purge();
551
+ });
552
+
553
+ // TODO: Ajouter plus de tests de validation pour editData, similaires à ceux de insertData
554
+ });
555
+
556
+ describe('$push operation using patchData', () =>{
557
+ describe('$push sur tableau existant', () => {
558
+ let docToPatchId;
559
+ let setupContext;
560
+
561
+ beforeEach(async () => {
562
+ setupContext = await setupTestContext();
563
+ const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
564
+
565
+ const initialData = {
566
+ stringRequired: 'InitialForPush',
567
+ stringUnique: 'UniqueForPush',
568
+ enumField: 'alpha',
569
+ passwordField: 'pushPass',
570
+ arrayString: ['tag1'],
571
+ arrayNumber: [100]
572
+ };
573
+ const insertResult = await insertData(comprehensiveTestModelDefinition.name, initialData, {}, currentTestUser, false);
574
+ docToPatchId = insertResult.insertedIds[0];
575
+ });
576
+
577
+ afterEach(async () => {
578
+ await setupContext.purge();
579
+ });
580
+
581
+ it('devrait ajouter une seule valeur à un tableau existant', async () => {
582
+ const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
583
+
584
+ const result = await patchData(comprehensiveTestModelDefinition.name, { _id: docToPatchId }, { arrayString: { $push: 'tag2' } }, {}, currentTestUser);
585
+
586
+ expect(result.success, `patchData avec $push a échoué: ${result.error}`).toBe(true);
587
+ expect(result.modifiedCount).toBe(1);
588
+
589
+ const patchedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docToPatchId) });
590
+ expect(patchedDoc.arrayString).toEqual(['tag1', 'tag2']);
591
+ });
592
+
593
+ it('devrait ajouter plusieurs valeurs à un tableau existant', async () => {
594
+ const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
595
+
596
+ const result = await patchData(comprehensiveTestModelDefinition.name, { _id: docToPatchId }, { arrayNumber: { $push: [200, 300] } }, {}, currentTestUser);
597
+
598
+ expect(result.success, `patchData avec $push multiple a échoué: ${result.error}`).toBe(true);
599
+ expect(result.modifiedCount).toBe(1);
600
+
601
+ const patchedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docToPatchId) });
602
+ expect(patchedDoc.arrayNumber).toEqual([100, 200, 300]);
603
+ });
604
+
605
+ it('devrait rejeter un push si la valeur est invalide', async () => {
606
+ const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
607
+
608
+ // Tenter de pusher un nombre dans un tableau de strings
609
+ const result = await patchData(comprehensiveTestModelDefinition.name, { _id: docToPatchId }, { arrayString: { $push: 12345 } }, {}, currentTestUser);
610
+
611
+ expect(result.success).toBe(false);
612
+ expect(result.error).toContain("Valeur invalide fournie pour le tableau 'arrayString'");
613
+ });
614
+
615
+ it('devrait rejeter un push si une des valeurs du tableau est invalide', async () => {
616
+ const { currentTestUser, comprehensiveTestModelDefinition } = setupContext;
617
+
618
+ // Tenter de pusher un tableau contenant une valeur invalide
619
+ const result = await patchData(comprehensiveTestModelDefinition.name, { _id: docToPatchId }, { arrayNumber: { $push: [400, 'not-a-number', 500] } }, {}, currentTestUser);
620
+
621
+ expect(result.success).toBe(false);
622
+ expect(result.error).toContain("Valeur invalide fournie pour le tableau 'arrayNumber'");
623
+ });
624
+
625
+ });
626
+ describe('$push sur un tableau inexistant', () => {
627
+ it('devrait créer le tableau et pusher les valeurs s\'il n\'existe pas', async () => {
628
+ // On utilise un setup de contexte complètement isolé pour ce test
629
+ const { currentTestUser, comprehensiveTestModelDefinition, purge } = await setupTestContext();
630
+
631
+ const initialData = { stringRequired: 'NoArrayField', stringUnique: 'UniqueForNoArrayPush', enumField: 'beta', passwordField: 'pass' };
632
+ const insertResult = await insertData(comprehensiveTestModelDefinition.name, initialData, {}, currentTestUser, false);
633
+ const newDocId = insertResult.insertedIds[0];
634
+
635
+ const result = await patchData(comprehensiveTestModelDefinition.name, { _id: newDocId }, { arrayString: { $push: ['first', 'second'] } }, {}, currentTestUser);
636
+
637
+ expect(result.success, `patchData sur un tableau inexistant a échoué: ${result.error}`).toBe(true);
638
+ expect(result.modifiedCount).toBe(1);
639
+
640
+ const patchedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(newDocId) });
641
+ expect(patchedDoc.arrayString).toEqual(['first', 'second']);
642
+ await purge();
643
+ });
644
+ });
645
+ });
646
+
647
+ // --- Tests deleteData (repris de votre exemple, peuvent être gardés tels quels ou adaptés) ---
648
+ describe('deleteData (tests existants)', () => {
649
+ it('supprimer des données par ID (deleteData)', async () => {
650
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
651
+ const dataToDelete = { name: 'Objet à supprimer', value: 300 };
652
+ // Utiliser le modèle simple pour ces tests delete pour ne pas avoir à remplir tous les champs requis de comprehensive
653
+ const simpleModel = { name: 'simpleDeleteModel', _user: currentTestUser.username, description: '', fields: [{ name: 'name', type: 'string' }, { name: 'value', type: 'number' }] };
654
+ await testModelsColInstance.insertOne(simpleModel);
655
+
656
+ const insertResult = await insertData(simpleModel.name, dataToDelete, {}, currentTestUser, false);
657
+ const docId = insertResult.insertedIds[0];
658
+
659
+ const deleteResult = await deleteData(simpleModel.name, [docId], currentTestUser);
660
+
661
+ expect(deleteResult.success).toBe(true);
662
+ expect(deleteResult.deletedCount).toBe(1);
663
+
664
+ const deletedDoc = await testDatasColInstance.findOne({ _id: new ObjectId(docId) });
665
+ expect(deletedDoc).toBeNull();
666
+ await purge();
667
+ await purge();
668
+ });
669
+
670
+ it('supprimer des données par filtre (deleteData)', async () => {
671
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
672
+ const simpleModel = { name: 'deleteModelTest', _user: currentTestUser.username, description: '',fields: [{ name: 'name', type: 'string' }, { name: 'value', type: 'number' }] };
673
+ await testModelsColInstance.insertOne(simpleModel);
674
+
675
+ await insertData(simpleModel.name, { name: 'Item Filtrable 1', value: 401 }, {}, currentTestUser, false);
676
+ await insertData(simpleModel.name, { name: 'Item Filtrable 2', value: 402 }, {}, currentTestUser, false);
677
+ await insertData(simpleModel.name, { name: 'Autre Item', value: 403 }, {}, currentTestUser, false);
678
+
679
+ const filter = { $regexMatch: {
680
+ input: '$name',
681
+ regex: "^Item Filtrable"
682
+ } };
683
+ const deleteResult = await deleteData(simpleModel.name, filter, currentTestUser);
684
+
685
+ expect(deleteResult.success).toBe(true);
686
+ expect(deleteResult.deletedCount).toBe(2);
687
+
688
+ const remainingCount = await testDatasColInstance.countDocuments({ _user: currentTestUser.username, _model: simpleModel.name, name: { $regex: "^Item Filtrable" } });
689
+ expect(remainingCount).toBe(0);
690
+ const totalRemaining = await testDatasColInstance.countDocuments({ _user: currentTestUser.username, _model: simpleModel.name });
691
+ expect(totalRemaining).toBe(1);
692
+ await purge();
693
+ });
694
+ });
695
+
696
+ describe('searchData avec comprehensiveTestModel et filtres $find', async () => {
697
+ let docId1, docId2;
698
+ let relatedDocId_A1, relatedDocId_A2, relatedDocId_B1;
699
+
700
+ const initTest =async () => {
701
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
702
+
703
+ // Insérer des documents liés
704
+ const relA1 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_A1', relatedValue: 101 }, {}, currentTestUser, false);
705
+ relatedDocId_A1 = relA1.insertedIds[0].toString();
706
+ const relA2 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_A2', relatedValue: 102 }, {}, currentTestUser, false);
707
+ relatedDocId_A2 = relA2.insertedIds[0].toString();
708
+ const relB1 = await insertData(relatedModelDefinition.name, { relatedName: 'Rel_Search_B1', relatedValue: 201 }, {}, currentTestUser, false);
709
+ relatedDocId_B1 = relB1.insertedIds[0].toString();
710
+
711
+ // Insérer des documents principaux
712
+ const data1 = {
713
+ stringRequired: 'SearchDoc1',
714
+ stringUnique: 'UniqueSearch1',
715
+ enumField: 'alpha',
716
+ passwordField: 'searchPass1',
717
+ relationSingle: relatedDocId_A1,
718
+ relationMultiple: [relatedDocId_A1, relatedDocId_A2],
719
+ number: 10
720
+ };
721
+ const insertResult1 = await insertData(comprehensiveTestModelDefinition.name, data1, {}, currentTestUser, false);
722
+ docId1 = insertResult1.insertedIds[0].toString();
723
+
724
+ const data2 = {
725
+ stringRequired: 'SearchDoc2',
726
+ stringUnique: 'UniqueSearch2',
727
+ enumField: 'beta',
728
+ passwordField: 'searchPass2',
729
+ relationSingle: relatedDocId_B1,
730
+ relationMultiple: [relatedDocId_B1],
731
+ number: 20
732
+ };
733
+ const insertResult2 = await insertData(comprehensiveTestModelDefinition.name, data2, {}, currentTestUser, false);
734
+ docId2 = insertResult2.insertedIds[0].toString();
735
+
736
+ return { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge };
737
+ };
738
+
739
+ it('devrait trouver un document par une condition sur une relation simple ($find)', async () => {
740
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
741
+ const searchParams = {
742
+ model: comprehensiveTestModelDefinition.name,
743
+ filter: {
744
+ relationSingle: {
745
+ "$find": { "$and": [{"$eq": ["$$this.relatedValue", 101]}] }
746
+ }
747
+ },
748
+ autoExpand: true,
749
+ depth: 8
750
+ };
751
+ const { data, count } = await searchData(searchParams, currentTestUser);
752
+ expect(count).toBe(1);
753
+ expect(data).toHaveLength(1);
754
+ expect(data[0]._id.toString()).toBe(docId1.toString());
755
+ // Vérifier que la relation est bien populée si depth > 0
756
+ expect(data[0].relationSingle).toBeInstanceOf(Object);
757
+ expect(data[0].relationSingle.relatedName).toBe('Rel_Search_A1');
758
+ await purge();
759
+ });
760
+
761
+ it('ne devrait pas trouver de document si la condition $find sur relation simple ne correspond pas', async () => {
762
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
763
+ const searchParams = {
764
+ model: comprehensiveTestModelDefinition.name,
765
+ filter: {
766
+ relationSingle: {
767
+ "$find": { "$eq": ["$$this.relatedName", "NonExistentRelName"] }
768
+ }
769
+ },
770
+ depth: 1
771
+ };
772
+ const { data, count } = await searchData(searchParams, currentTestUser);
773
+ expect(count).toBe(0);
774
+ expect(data).toHaveLength(0);
775
+ await purge();
776
+ });
777
+
778
+ it('devrait trouver des documents par une condition sur une relation multiple ($find)', async () => {
779
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
780
+ const searchParams = {
781
+ model: comprehensiveTestModelDefinition.name,
782
+ filter: {
783
+ $and: [{$ne:["$stringRequired", null]}, {
784
+ relationMultiple: { // Le champ qui est un tableau de relations
785
+ "$find": {"$eq": ["$$this.relatedValue", 102]} // Condition sur les documents liés
786
+ }
787
+ }]
788
+ },
789
+ depth: 1
790
+ };
791
+ const { data, count } = await searchData(searchParams, currentTestUser);
792
+ expect(count).toBe(1);
793
+ expect(data).toHaveLength(1);
794
+ expect(data[0]._id.toString()).toBe(docId1.toString());
795
+ // Note: searchData with depth > 0 will populate the relationMultiple array
796
+ expect(data[0].relationMultiple).toBeInstanceOf(Array);
797
+ expect(data[0].relationMultiple.some(rel => rel.relatedValue === 102)).toBe(true);
798
+ await purge();
799
+ });
800
+
801
+
802
+ it('devrait trouver des documents en combinant un filtre normal et un filtre $find', async () => {
803
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
804
+ const searchParams = {
805
+ model: comprehensiveTestModelDefinition.name,
806
+ filter: {
807
+ stringRequired: "SearchDoc1", // Filtre sur le modèle principal
808
+ relationSingle: {
809
+ "$find": {"$gt": ["$$this.relatedValue", 100] } // Filtre sur la relation
810
+ }
811
+ },
812
+ depth: 1
813
+ };
814
+ const { data, count } = await searchData(searchParams,currentTestUser);
815
+ expect(count).toBe(1);
816
+ expect(data).toHaveLength(1);
817
+ expect(data[0]._id.toString()).toBe(docId1.toString());
818
+ await purge();
819
+ });
820
+
821
+ it('devrait retourner un tableau vide si $find ne correspond à aucun document lié', async () => {
822
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
823
+ const searchParams = {
824
+ model: comprehensiveTestModelDefinition.name,
825
+ filter: {
826
+ relationMultiple: {
827
+ "$find": { "$eq": ["$$this.relatedName", "NonExistentRelNameForMultiple"] }
828
+ }
829
+ },
830
+ depth: 1
831
+ };
832
+ const { data, count } = await searchData(searchParams,currentTestUser);
833
+ expect(count).toBe(0);
834
+ expect(data).toHaveLength(0);
835
+ await purge();
836
+ });
837
+
838
+ // --- NOUVEAUX TESTS POUR LE SHORTHAND $find ---
839
+
840
+ it('devrait trouver un document en utilisant un filtre $find simple (shorthand)', async () => {
841
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
842
+ const searchParams = {
843
+ model: comprehensiveTestModelDefinition.name,
844
+ filter: {
845
+ relationSingle: {
846
+ "$find": { "relatedValue": 101 } // Utilisation du shorthand { champ: valeur }
847
+ }
848
+ },
849
+ depth: 1
850
+ };
851
+ const { data, count } = await searchData(searchParams, currentTestUser);
852
+ expect(count).toBe(1);
853
+ expect(data).toHaveLength(1);
854
+ expect(data[0]._id.toString()).toBe(docId1.toString());
855
+ await purge();
856
+ });
857
+
858
+ it('devrait trouver un document en utilisant un filtre $find simple avec plusieurs conditions (AND implicite)', async () => {
859
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
860
+ const searchParams = {
861
+ model: comprehensiveTestModelDefinition.name,
862
+ filter: {
863
+ relationSingle: {
864
+ "$find": {
865
+ "relatedName": "Rel_Search_A1",
866
+ "relatedValue": 101
867
+ }
868
+ }
869
+ },
870
+ depth: 1
871
+ };
872
+ const { data, count } = await searchData(searchParams, currentTestUser);
873
+ expect(count).toBe(1);
874
+ expect(data).toHaveLength(1);
875
+ expect(data[0]._id.toString()).toBe(docId1.toString());
876
+ await purge();
877
+ });
878
+
879
+ it('ne devrait pas trouver de document si une des conditions du $find simple échoue', async () => {
880
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
881
+ const searchParams = {
882
+ model: comprehensiveTestModelDefinition.name,
883
+ filter: {
884
+ relationSingle: {
885
+ "$find": {
886
+ "relatedName": "Rel_Search_A1", // Correspond
887
+ "relatedValue": 999 // Ne correspond pas
888
+ }
889
+ }
890
+ },
891
+ depth: 1
892
+ };
893
+ const { data, count } = await searchData(searchParams, currentTestUser);
894
+ expect(count).toBe(0);
895
+ expect(data).toHaveLength(0);
896
+ await purge();
897
+ });
898
+
899
+ it('devrait trouver un document via une relation multiple en utilisant un filtre $find simple', async () => {
900
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await initTest();
901
+ const searchParams = {
902
+ model: comprehensiveTestModelDefinition.name,
903
+ filter: {
904
+ relationMultiple: {
905
+ "$find": { "relatedValue": 102 } // Devrait trouver docId1 car il a une relation vers relA2
906
+ }
907
+ },
908
+ depth: 1
909
+ };
910
+ const { data, count } = await searchData(searchParams, currentTestUser);
911
+ expect(count).toBe(1);
912
+ expect(data).toHaveLength(1);
913
+ expect(data[0]._id.toString()).toBe(docId1.toString());
914
+ await purge();
915
+ });
916
+
917
+ });
918
+
919
+ describe('installPack', () => {
920
+ let testPacksColInstance;
921
+
922
+ beforeAll(() => {
923
+ // Note: l'initialisation est déjà faite dans le beforeAll global.
924
+ // On s'assure juste d'avoir une référence si on en a besoin spécifiquement ici.
925
+ // Si vous n'avez pas suivi l'étape 1, vous pouvez l'initialiser ici.
926
+ testPacksColInstance = getCollection('packs');
927
+ });
928
+
929
+ beforeEach(async () => {
930
+ // On supprime les données de ce test
931
+ });
932
+
933
+ it('devrait installer un pack, créer les modèles et insérer les données avec relations via $link', async () => {
934
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
935
+ // 1. Définir le pack de données mock
936
+ const mockPack = {
937
+ private: false,
938
+ name: 'test-pack-for-install',
939
+ models: [
940
+ {
941
+ name: 'packUser',
942
+ description: 'Modèle pour les utilisateurs du pack',
943
+ fields: [
944
+ { name: 'username', type: 'string', required: true, unique: true },
945
+ { name: 'level', type: 'number', default: 1 }
946
+ ]
947
+ },
948
+ {
949
+ name: 'packItem',
950
+ description: 'Modèle pour les items du pack',
951
+ fields: [
952
+ { name: 'itemName', type: 'string', required: true },
953
+ { name: 'owner', type: 'relation', relation: 'packUser' }
954
+ ]
955
+ }
956
+ ],
957
+ data: {
958
+ all: { // 'all' pour les données non spécifiques à une langue
959
+ packUser: [
960
+ { username: 'playerOne' },
961
+ { username: 'playerTwo', level: 5 }
962
+ ],
963
+ packItem: [
964
+ // La nouvelle syntaxe $link pour résoudre les relations
965
+ { itemName: 'Sword', owner: { $link: { _model: 'packUser', username: 'playerOne' } } },
966
+ { itemName: 'Shield', owner: { $link: { _model: 'packUser', username: 'playerTwo' } } }
967
+ ]
968
+ }
969
+ }
970
+ };
971
+
972
+ // 2. Insérer le pack dans la collection pour le rendre disponible au test
973
+ const packInsertResult = await testPacksColInstance.insertOne(mockPack);
974
+ const packId = packInsertResult.insertedId.toString();
975
+
976
+ // 3. Appeler la fonction à tester avec la nouvelle signature
977
+ const result = await installPack(packId, currentTestUser, 'en');
978
+
979
+ // 4. Assertions sur le résumé de l'installation
980
+ expect(result.success, `L'installation du pack a échoué: ${result.errors?.join('; ')}`).toBe(true);
981
+ expect(result.summary.models.installed).toHaveLength(2);
982
+ expect(result.summary.datas.inserted).toBe(4); // 2 utilisateurs + 2 items
983
+ expect(result.summary.datas.updated).toBe(2); // 2 items mis à jour avec la relation
984
+
985
+ // 5. Vérifier la création des modèles
986
+ const userModel = await testModelsColInstance.findOne({ name: 'packUser', _user: currentTestUser.username });
987
+ expect(userModel).not.toBeNull();
988
+ const itemModel = await testModelsColInstance.findOne({ name: 'packItem', _user: currentTestUser.username });
989
+ expect(itemModel).not.toBeNull();
990
+ expect(itemModel.fields.find(f => f.name === 'owner').relation).toBe('packUser');
991
+
992
+ // 6. Vérifier l'insertion des données
993
+ const insertedUsers = await testDatasColInstance.find({ _model: 'packUser', _user: currentTestUser.username }).toArray();
994
+ expect(insertedUsers).toHaveLength(2);
995
+ const insertedItems = await testDatasColInstance.find({ _model: 'packItem', _user: currentTestUser.username }).toArray();
996
+ expect(insertedItems).toHaveLength(2);
997
+
998
+ // 7. Vérifier la résolution des relations via $link
999
+ const playerOne = insertedUsers.find(u => u.username === 'playerOne');
1000
+ const sword = insertedItems.find(i => i.itemName === 'Sword');
1001
+ expect(sword.owner.toString()).toBe(playerOne._id.toString());
1002
+
1003
+ const playerTwo = insertedUsers.find(u => u.username === 'playerTwo');
1004
+ const shield = insertedItems.find(i => i.itemName === 'Shield');
1005
+ expect(shield.owner.toString()).toBe(playerTwo._id.toString());
1006
+ await purge();
1007
+ }, 10000);
1008
+
1009
+ it("ne devrait installer que les modèles valides d'un pack et rapporter les erreurs", async () => {
1010
+ const { currentTestUser, comprehensiveTestModelDefinition, relatedModelDefinition, purge } = await setupTestContext();
1011
+ const mockPackWithInvalidModel = {
1012
+ name: 'invalid-model-pack',
1013
+ private: false,
1014
+ models: [
1015
+ {
1016
+ // 'name' est requis, on l'omet pour causer une erreur
1017
+ description: 'Modèle invalide sans nom',
1018
+ fields: []
1019
+ },
1020
+ { // Un modèle valide pour s'assurer que l'installation continue
1021
+ name: 'validPackModel',
1022
+ description: 'Un modèle valide',
1023
+ fields: [{ name: 'title', type: 'string' }]
1024
+ }
1025
+ ],
1026
+ data: {}
1027
+ };
1028
+
1029
+ const packInsertResult = await testPacksColInstance.insertOne(mockPackWithInvalidModel);
1030
+ const packId = packInsertResult.insertedId.toString();
1031
+
1032
+ const result = await installPack(packId, currentTestUser, 'en');
1033
+
1034
+ // Le succès global est faux s'il y a des erreurs
1035
+ expect(result.success).toBe(false);
1036
+ expect(result.errors).toHaveLength(1);
1037
+ expect(result.errors[0]).toContain("Failed to install model 'unknown'"); // Le nom est inconnu
1038
+
1039
+ // Vérifier le résumé de l'option
1040
+ expect(result.summary.models.failed).toHaveLength(1);
1041
+ expect(result.summary.models.failed[0]).toBe('unknown');
1042
+ expect(result.summary.models.installed).toHaveLength(1);
1043
+ expect(result.summary.models.installed[0]).toBe('validPackModel');
1044
+
1045
+ // Vérifier en BDD que le modèle valide a bien été créé
1046
+ const validModel = await testModelsColInstance.findOne({ name: 'validPackModel', _user: currentTestUser.username });
1047
+ expect(validModel).not.toBeNull();
1048
+ await purge();
1049
+ });
1050
+
1051
+ it('devrait correctement insérer des données JSON depuis un pack', async () => {
1052
+ const { currentTestUser } = await setupTestContext();
1053
+ // 1. Définir le pack avec des données JSON
1054
+ const mockPackWithJson = {
1055
+ name: 'test-pack-with-json',
1056
+ private: false,
1057
+ models: [
1058
+ {
1059
+ name: 'packConfig',
1060
+ description: 'Modèle avec un champ JSON',
1061
+ fields: [
1062
+ { name: 'configName', type: 'string', required: true },
1063
+ { name: 'settings', type: 'code', language: 'json' }
1064
+ ]
1065
+ }
1066
+ ],
1067
+ data: {
1068
+ all: {
1069
+ packConfig: [
1070
+ {
1071
+ configName: 'mainConfig',
1072
+ settings: {
1073
+ theme: 'dark',
1074
+ notifications: {
1075
+ enabled: true,
1076
+ level: 'important'
1077
+ }
1078
+ }
1079
+ }
1080
+ ]
1081
+ }
1082
+ }
1083
+ };
1084
+
1085
+ // 2. Insérer le pack
1086
+ const packInsertResult = await testPacksColInstance.insertOne(mockPackWithJson);
1087
+ const packId = packInsertResult.insertedId.toString();
1088
+
1089
+ // 3. Appeler la fonction à tester
1090
+ const result = await installPack(packId, currentTestUser, 'en');
1091
+
1092
+ // 4. Assertions
1093
+ expect(result.success, `L'installation du pack JSON a échoué: ${result.errors?.join('; ')}`).toBe(true);
1094
+ expect(result.summary.models.installed).toHaveLength(1);
1095
+ expect(result.summary.datas.inserted).toBe(1);
1096
+
1097
+ // 5. Vérifier les données insérées en BDD
1098
+ const datasCol = await getCollectionForUser(currentTestUser);
1099
+ const insertedConfig = await datasCol.findOne({ _model: 'packConfig', _user: currentTestUser.username });
1100
+
1101
+ expect(insertedConfig).not.toBeNull();
1102
+ expect(insertedConfig.configName).toBe('mainConfig');
1103
+ expect(typeof insertedConfig.settings).toBe('object');
1104
+ expect(insertedConfig.settings).toEqual({
1105
+ theme: 'dark',
1106
+ notifications: {
1107
+ enabled: true,
1108
+ level: 'important'
1109
+ }
1110
+ });
1111
+
1112
+ await getCollection('packs').deleteMany({ _id: packId});
1113
+ });
1114
+ });
1115
+ // In test/data.integration.test.js
1116
+
1117
+ describe('relationFilter validation', () => {
1118
+ let user;
1119
+ let activeProductId, inactiveProductId;
1120
+
1121
+ // Model names are kept in French to match the database, but variables are in English.
1122
+ const productModel = {
1123
+ name: 'produitTestFiltre',
1124
+ description: '',
1125
+ fields: [
1126
+ { name: 'name', type: 'string' },
1127
+ { name: 'actif', type: 'boolean', default: false }
1128
+ ]
1129
+ };
1130
+ const orderModel = {
1131
+ name: 'commandeTestFiltre',
1132
+ description: '',
1133
+ fields: [
1134
+ { name: 'ref', type: 'string' },
1135
+ {
1136
+ name: 'produit',
1137
+ type: 'relation',
1138
+ relation: 'produitTestFiltre',
1139
+ relationFilter: { "$eq":["$actif", true] } // Only link active products
1140
+ }
1141
+ ]
1142
+ };
1143
+
1144
+ // Set up the context once for all tests in this describe block.
1145
+ beforeAll(async () => {
1146
+ user = await engine.userProvider.findUserByUsername('demo');
1147
+ // Cleanup before starting to ensure a clean state
1148
+ await deleteModels({ name: productModel.name, _user: user.username });
1149
+ await deleteModels({ name: orderModel.name, _user: user.username });
1150
+ await deleteData(productModel.name, {}, user);
1151
+ await deleteData(orderModel.name, {}, user);
1152
+
1153
+ // Create models
1154
+ await createModel({ ...productModel, _user: user.username });
1155
+ await createModel({ ...orderModel, _user: user.username });
1156
+
1157
+ // Create test data
1158
+ const activeProduct = await insertData(productModel.name, { name: 'Active Product', actif: true }, {}, user);
1159
+ const inactiveProduct = await insertData(productModel.name, { name: 'Inactive Product', actif: false }, {}, user);
1160
+
1161
+ activeProductId = activeProduct.insertedIds[0];
1162
+ inactiveProductId = inactiveProduct.insertedIds[0];
1163
+ });
1164
+
1165
+ // Clean up everything after all tests in this block have run.
1166
+ afterAll(async () => {
1167
+ await deleteModels({ name: productModel.name, _user: user.username });
1168
+ await deleteModels({ name: orderModel.name, _user: user.username });
1169
+ await deleteData(productModel.name, {}, user);
1170
+ await deleteData(orderModel.name, {}, user);
1171
+ const coll = await getCollectionForUser(user);
1172
+ await coll.drop();
1173
+ });
1174
+
1175
+ it('should ALLOW inserting data with a valid relation', async () => {
1176
+ const result = await insertData(orderModel.name, { ref: 'CMD-OK', produit: activeProductId }, {}, user);
1177
+ expect(result.success).toBe(true);
1178
+ expect(result.insertedIds).toHaveLength(1);
1179
+ // Cleanup the created order for test isolation
1180
+ await deleteData(orderModel.name, result.insertedIds, user);
1181
+ });
1182
+
1183
+ it('should REJECT inserting data with a relation that does not respect the filter', async () => {
1184
+ const result = await insertData(orderModel.name, { ref: 'CMD-FAIL', produit: inactiveProductId }, {}, user);
1185
+ expect(result.success).toBe(false);
1186
+ expect(result.error).toContain('produit');
1187
+ });
1188
+
1189
+ it('should ALLOW updating data with a valid relation', async () => {
1190
+ // First, create a valid order
1191
+ const initialOrder = await insertData(orderModel.name, { ref: 'CMD-TO-EDIT', produit: activeProductId }, {}, user);
1192
+
1193
+ // Update it (even with the same value, this tests the code path)
1194
+ const result = await editData(orderModel.name, { _id: initialOrder.insertedIds[0] }, { produit: activeProductId }, {}, user);
1195
+
1196
+ expect(result.success).toBe(true);
1197
+ // The hash might not change if only metadata like _updatedAt changes, so modifiedCount can be 0 or 1.
1198
+ expect(result.modifiedCount).toBeGreaterThanOrEqual(0);
1199
+
1200
+ // Cleanup
1201
+ await deleteData(orderModel.name, initialOrder.insertedIds, user);
1202
+ });
1203
+
1204
+ it('should REJECT updating data with a relation that does not respect the filter', async () => {
1205
+ // First, create a valid order
1206
+ const initialOrder = await insertData(orderModel.name, { ref: 'CMD-TO-EDIT-FAIL', produit: activeProductId }, {}, user);
1207
+
1208
+ // Attempt to link it to an inactive product, expecting it to fail.
1209
+ const result = await editData(orderModel.name, { _id: initialOrder.insertedIds[0] }, { produit: inactiveProductId }, {}, user);
1210
+ expect(result.success).toBe(false);
1211
+ expect(result.error).toContain('produit');
1212
+ // Cleanup
1213
+ await deleteData(orderModel.name, initialOrder.insertedIds, user);
1214
+ });
1215
+ });
1216
+
1217
+ describe('searchData avec filtre _env', () => {
1218
+ let user, testModel, noEnvDocId;
1219
+
1220
+ beforeAll(async () => {
1221
+ // Setup
1222
+ const context = await setupTestContext();
1223
+ user = context.currentTestUser;
1224
+ testModel = {
1225
+ name: generateUniqueName('envTestModel'),
1226
+ description: '',
1227
+ _user: user.username,
1228
+ fields: [{ name: 'title', type: 'string' }]
1229
+ };
1230
+ await createModel(testModel);
1231
+
1232
+ // Insert test data
1233
+ await insertData(testModel.name, { title: 'Prod Data', _env: 'prod' }, {}, user, false);
1234
+ await insertData(testModel.name, { title: 'Staging Data', _env: 'staging' }, {}, user, false);
1235
+ const noEnvResult = await insertData(testModel.name, { title: 'No Env Data' }, {}, user, false);
1236
+ noEnvDocId = noEnvResult.insertedIds[0];
1237
+ });
1238
+
1239
+ afterAll(async () => {
1240
+ // Cleanup
1241
+ await deleteModels({ name: testModel.name, _user: user.username });
1242
+ await deleteData(testModel.name, {}, user);
1243
+ });
1244
+
1245
+ it('devrait trouver uniquement les données avec _env="prod"', async () => {
1246
+ const { data, count } = await searchData({
1247
+ model: testModel.name,
1248
+ env: 'prod'
1249
+ }, user);
1250
+ expect(count).toBe(1);
1251
+ expect(data).toHaveLength(1);
1252
+ expect(data[0].title).toBe('Prod Data');
1253
+ });
1254
+
1255
+ it('devrait trouver uniquement les données avec _env="staging"', async () => {
1256
+ const { data, count } = await searchData({
1257
+ model: testModel.name,
1258
+ env: 'staging'
1259
+ }, user);
1260
+ expect(count).toBe(1);
1261
+ expect(data).toHaveLength(1);
1262
+ expect(data[0].title).toBe('Staging Data');
1263
+ });
1264
+
1265
+ it('devrait trouver uniquement les données SANS _env spécifié', async () => {
1266
+ const { data, count } = await searchData({
1267
+ model: testModel.name
1268
+ // _env est omis intentionnellement
1269
+ }, user);
1270
+ expect(count).toBe(1);
1271
+ expect(data).toHaveLength(1);
1272
+ expect(data[0].title).toBe('No Env Data');
1273
+ });
1274
+
1275
+ it('ne devrait trouver aucune donnée pour un _env inexistant', async () => {
1276
+ const { data, count } = await searchData({
1277
+ model: testModel.name,
1278
+ env: 'nonexistent'
1279
+ }, user);
1280
+ expect(count).toBe(0);
1281
+ expect(data).toHaveLength(0);
1282
+ });
1283
+
1284
+ it('devrait permettre de modifier le champ _env via editData', async () => {
1285
+ const result = await editData(testModel.name, { _id: noEnvDocId }, { _env: 'newly_assigned' }, {}, user);
1286
+ expect(result.success).toBe(true);
1287
+ const { data, count } = await searchData({ model: testModel.name, env: 'newly_assigned' }, user);
1288
+ expect(count).toBe(1);
1289
+ expect(data[0]._id.toString()).toBe(noEnvDocId.toString());
1290
+ });
1291
+ });
1292
+
1293
+ describe('Data operations with permission filters', () => {
1294
+ let testUser, testSystemUser, permissionAdd, permissionEdit, permissionDelete, role;
1295
+ let orderModelDef, pendingOrderId, shippedOrderId, otherUserOrderId;
1296
+
1297
+ beforeEach(async () => {
1298
+ // Création d'un utilisateur de test isolé
1299
+ const username = generateUniqueName('perm_filter_user');
1300
+ testUser = { _id: new ObjectId(), username: username, _user: 'testSystemUser', _model: 'user', roles: [] };
1301
+ testSystemUser = { _id: new ObjectId(), username: 'testSystemUser', roles: ['API_ADMIN'] }; // L'utilisateur système a tous les droits
1302
+ const collection = await getCollectionForUser(testUser);
1303
+
1304
+ // 1. Créer les modèles nécessaires
1305
+ await createModel({ name: 'permission', description:'t', _user: testSystemUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'filter', type: 'code', language: 'json' }, { name: 'description', type: 'string' }] });
1306
+ await createModel({ name: 'role',description:'t', _user: testSystemUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'permissions', type: 'relation', relation: 'permission', multiple: true }] });
1307
+ // Le modèle 'orderTest' doit exister pour les deux utilisateurs : testUser (pour les opérations de test) et testSystemUser (pour le setup)
1308
+ orderModelDef = { name: 'orderTest',description:'t', _user: testSystemUser.username, fields: [{ name: 'status', type: 'string' }, { name: 'amount', type: 'number' }, { name: 'customer', type: 'string' }] };
1309
+ const t = await createModel(orderModelDef);
1310
+
1311
+ // 2. Créer les permissions avec filtres pour les tests
1312
+ const permissionsToCreate = [
1313
+ // On a besoin de ces permissions pour que testUser puisse créer le rôle et les permissions
1314
+ { name: 'API_ADD_DATA_permission', _user: testSystemUser.username },
1315
+ { name: 'API_ADD_DATA_role', _user: testSystemUser.username },
1316
+ // Permissions de l'application
1317
+ { name: 'API_SEARCH_DATA_orderTest' }, // <-- AJOUTER CETTE LIGNE
1318
+ { name: 'API_ADD_DATA_orderTest', filter: { "$lt" : ["$amount", 1000] }, description: "Filter for adding orders" },
1319
+ { name: 'API_EDIT_DATA_orderTest', filter: { status: 'pending' } },
1320
+ { name: 'API_DELETE_DATA_orderTest', filter: { customer: testUser._id.toString() } }
1321
+ ];
1322
+ // On insère les permissions pour les deux utilisateurs pour que le rôle soit valide pour testUser
1323
+ let permResult = await insertData('permission', permissionsToCreate, {}, testSystemUser, false, false, false); // Pour le setup
1324
+ expect(permResult.success, `La création des permissions a échoué: ${permResult.error}`).toBe(true);
1325
+
1326
+ // 3. Récupérer les IDs des permissions et créer le rôle
1327
+ // On se base sur les permissions de l'utilisateur de test pour créer son rôle
1328
+ const allPerms = await (await getCollectionForUser(testSystemUser)).find({ _model: 'permission' }).toArray();
1329
+ const permIds = allPerms.map(p => p._id.toString());
1330
+
1331
+ // On insère le rôle pour les deux utilisateurs
1332
+ let roleRes = await insertData('role', { name: 'OrderManager', permissions: permIds }, {}, testSystemUser, false, false, false);
1333
+ expect(roleRes.success, `La création du rôle a échoué: ${roleRes.error}`).toBe(true);
1334
+ role = roleRes.insertedIds[0];
1335
+
1336
+ // 4. Assigner le rôle à l'utilisateur de test
1337
+ testUser.roles = [role.toString()];
1338
+
1339
+ // 5. Créer les données de test avec l'utilisateur de test lui-même.
1340
+ // Pour cela, on lui donne temporairement les droits d'admin pour le setup.
1341
+ const originalRoles = testUser.roles;
1342
+ testUser.roles = ['API_ADMIN']; // Droits temporaires pour créer les données
1343
+ const pendingOrder = await insertData('orderTest', { status: 'pending', amount: 100, customer: testUser._id.toString() }, {}, testUser, false, false, false);
1344
+ pendingOrderId = pendingOrder.insertedIds[0];
1345
+ const shippedOrder = await insertData('orderTest', { status: 'shipped', amount: 200, customer: testUser._id.toString() }, {}, testUser, false, false, false);
1346
+ shippedOrderId = shippedOrder.insertedIds[0];
1347
+ const otherOrder = await insertData('orderTest', { status: 'pending', amount: 300, customer: 'other_user_id' }, {}, testUser, false, false, false);
1348
+ otherUserOrderId = otherOrder.insertedIds[0];
1349
+ testUser.roles = originalRoles; // Restaurer les vrais rôles pour le test
1350
+ });
1351
+
1352
+ afterEach(async () => {
1353
+ await (await getCollectionForUser(testUser)).deleteMany({ _user: testUser.username });
1354
+ await (await getCollectionForUser(testSystemUser)).deleteMany({ _user: testSystemUser.username });
1355
+ await deleteModels({ _user: testUser.username });
1356
+ await deleteModels({ _user: testSystemUser.username });
1357
+ });
1358
+
1359
+ afterAll(async () =>{
1360
+ await purgeData(testUser);
1361
+ await purgeData(testSystemUser);
1362
+ await datasCollection.drop();
1363
+ })
1364
+
1365
+ // --- TEST POUR insertData ---
1366
+ it('should REJECT inserting data that violates the permission filter', async () => {
1367
+ // This should fail because the amount (1500) is not < 1000
1368
+ const result = await insertData('orderTest', { status: 'new', amount: 1500, customer: 'new_customer' }, {}, testUser, false, false);
1369
+
1370
+ expect(result.success).toBe(false);
1371
+
1372
+ });
1373
+
1374
+ it('should ALLOW inserting data that respects the permission filter', async () => {
1375
+ // This should succeed because the amount (500) is < 1000
1376
+ const result = await insertData('orderTest', { status: 'new', amount: 500, customer: 'new_customer' }, {}, testUser, false, false);
1377
+
1378
+ expect(result.success, `L'insertion aurait réussir: ${result.error}`).toBe(true);
1379
+ expect(result.insertedIds).toHaveLength(1);
1380
+ });
1381
+
1382
+ // --- TESTS POUR editData ---
1383
+ it('should ALLOW editing a document that matches the permission filter', async () => {
1384
+ // Le filtre de permission est { status: 'pending' }
1385
+ const result = await editData('orderTest', { _id: pendingOrderId }, { amount: 150 }, {}, testUser, false, false);
1386
+
1387
+ expect(result.success, `editData failed with error: ${result.error}`).toBe(true);
1388
+ expect(result.modifiedCount).toBeGreaterThanOrEqual(0); // Peut être 0 si le hash ne change pas
1389
+
1390
+ const coll = await getCollectionForUser(testUser);
1391
+ const doc = await coll.findOne({ _id: new ObjectId(pendingOrderId)});
1392
+ expect(doc.amount).toBe(150);
1393
+ });
1394
+
1395
+ it('should REJECT editing a document that does NOT match the permission filter', async () => {
1396
+ // On tente de modifier la commande 'shipped', mais la permission ne l'autorise que pour 'pending'
1397
+ const result = await editData('orderTest', { $eq: ['$_id', new ObjectId(shippedOrderId)] }, { amount: 250 }, {}, testUser, false, false);
1398
+
1399
+ expect(result.success).toBe(false);
1400
+ });
1401
+
1402
+ // --- TESTS POUR deleteData ---
1403
+ it('should ALLOW deleting a document that matches the permission filter', async () => {
1404
+ // La permission autorise la suppression des commandes où `customer` est l'ID de l'utilisateur
1405
+ const result = await deleteData('orderTest', { _id: new ObjectId(pendingOrderId) }, testUser, false, false);
1406
+
1407
+ expect(result.success, `deleteData failed with error: ${result.error}`).toBe(true);
1408
+ expect(result.deletedCount).toBe(1);
1409
+
1410
+ const doc = await (await getCollectionForUser(testUser)).findOne({ _id: new ObjectId(pendingOrderId) });
1411
+ expect(doc).toBeNull();
1412
+ });
1413
+
1414
+ it('should REJECT deleting a document that does NOT match the permission filter', async () => {
1415
+ // On tente de supprimer une commande qui n'appartient pas à l'utilisateur
1416
+ const result = await deleteData('orderTest', { _id: otherUserOrderId }, testUser, false, false);
1417
+
1418
+ // La fonction va trouver 0 document à supprimer après application du filtre de permission
1419
+ expect(result.success).toBe(true); // La fonction réussit, mais ne supprime rien
1420
+ expect(result.deletedCount).toBe(0);
1421
+
1422
+ const doc = await (await getCollectionForUser(testSystemUser)).findOne({ _id: new ObjectId(otherUserOrderId) });
1423
+ expect(doc).not.toBeNull(); // Le document existe toujours
1424
+ });
1425
+ });
1417
1426
  });