data-primals-engine 1.6.3 → 1.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -1009
- package/client/package-lock.json +64 -50
- package/client/package.json +6 -0
- package/client/src/App.scss +29 -0
- package/client/src/_variables.scss +3 -0
- package/doc/automation-workflows.md +102 -0
- package/doc/core-concepts.md +33 -0
- package/doc/custom-api-endpoints.md +40 -0
- package/doc/dashboards-kpis-charts.md +49 -0
- package/doc/data-management.md +120 -0
- package/doc/data-models.md +75 -0
- package/doc/index.md +14 -0
- package/doc/roles-permissions.md +43 -0
- package/doc/users.md +30 -0
- package/package.json +7 -5
- package/src/client.js +6 -4
- package/src/core.js +31 -12
- package/src/filter.js +72 -173
- package/src/modules/data/data.js +2 -1
- package/src/modules/data/data.operations.js +107 -69
- package/src/modules/data/data.relations.js +1 -1
- package/src/modules/mongodb.js +2 -1
- package/src/modules/swagger.js +25 -5
- package/src/modules/user.js +108 -78
- package/src/modules/workflow.js +138 -3
- package/swagger-en.yml +2308 -472
- package/swagger-fr.yml +487 -3
- package/test/core.test.js +341 -0
- package/test/data.integration.test.js +137 -2
- package/test/user.test.js +33 -29
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
sleep,
|
|
6
|
+
escapeRegex,
|
|
7
|
+
sequential,
|
|
8
|
+
isValidRegex,
|
|
9
|
+
escapeHtml,
|
|
10
|
+
isUnsecureKey,
|
|
11
|
+
parseSafeJSON,
|
|
12
|
+
isDate,
|
|
13
|
+
safeAssignObject,
|
|
14
|
+
debounce,
|
|
15
|
+
uuidv4,
|
|
16
|
+
cssProps,
|
|
17
|
+
removeDir,
|
|
18
|
+
wordWrap,
|
|
19
|
+
getObjectHash,
|
|
20
|
+
isPathRelativeTo,
|
|
21
|
+
isGUID,
|
|
22
|
+
isPlainObject,
|
|
23
|
+
escapeRegExp,
|
|
24
|
+
shuffle,
|
|
25
|
+
setSeed,
|
|
26
|
+
getRand,
|
|
27
|
+
getRandom,
|
|
28
|
+
randomDate,
|
|
29
|
+
isLightColor,
|
|
30
|
+
tryParseJson,
|
|
31
|
+
isIsoDate,
|
|
32
|
+
event_trigger,
|
|
33
|
+
event_on,
|
|
34
|
+
event_off,
|
|
35
|
+
slugify,
|
|
36
|
+
getFileExtension,
|
|
37
|
+
object_equals,
|
|
38
|
+
isValidPath,
|
|
39
|
+
stringToHslColor,
|
|
40
|
+
countKeys
|
|
41
|
+
} from '../src/core.js';
|
|
42
|
+
|
|
43
|
+
describe('Core Utility Functions', () => {
|
|
44
|
+
|
|
45
|
+
describe('isPlainObject', () => {
|
|
46
|
+
it('should return true for plain objects', () => {
|
|
47
|
+
expect(isPlainObject({})).toBe(true);
|
|
48
|
+
expect(isPlainObject({ a: 1 })).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return false for non-plain objects or other types', () => {
|
|
52
|
+
expect(isPlainObject([])).toBe(false);
|
|
53
|
+
expect(isPlainObject(null)).toBe(false);
|
|
54
|
+
expect(isPlainObject(new Date())).toBe(false);
|
|
55
|
+
expect(isPlainObject("string")).toBe(false);
|
|
56
|
+
expect(isPlainObject(123)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('slugify', () => {
|
|
61
|
+
it('should convert string to a slug', () => {
|
|
62
|
+
expect(slugify('Hello World!')).toBe('hello-world');
|
|
63
|
+
expect(slugify(' Test avec des espaces ')).toBe('test-avec-des-espaces');
|
|
64
|
+
expect(slugify('CaractèresSpéciaux-123', '-', true)).toBe('caracteresspeciaux-123');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('sleep', () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
vi.useFakeTimers();
|
|
71
|
+
});
|
|
72
|
+
afterEach(() => {
|
|
73
|
+
vi.useRealTimers();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should resolve after the specified duration', async () => {
|
|
77
|
+
const sleepPromise = sleep(1000);
|
|
78
|
+
vi.advanceTimersByTime(1000);
|
|
79
|
+
await expect(sleepPromise).resolves.toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
describe('getObjectHash', () => {
|
|
85
|
+
it('should generate a consistent hash for the same object', () => {
|
|
86
|
+
const obj1 = { name: 'test', value: 1 };
|
|
87
|
+
const obj2 = { value: 1, name: 'test' }; // Ordre différent
|
|
88
|
+
expect(getObjectHash(obj1)).toBe(getObjectHash(obj2));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should generate a different hash for different objects', () => {
|
|
92
|
+
const obj1 = { name: 'test', value: 1 };
|
|
93
|
+
const obj2 = { name: 'test', value: 2 };
|
|
94
|
+
expect(getObjectHash(obj1)).not.toBe(getObjectHash(obj2));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('escapeRegex and escapeRegExp', () => {
|
|
99
|
+
it('should escape special regex characters', () => {
|
|
100
|
+
const specialString = '.*+?^${}()|[]\\';
|
|
101
|
+
const expected = '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\';
|
|
102
|
+
expect(escapeRegex(specialString)).toBe(expected);
|
|
103
|
+
expect(escapeRegExp(specialString)).toBe(expected);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('sequential', () => {
|
|
108
|
+
it('should execute async tasks sequentially', async () => {
|
|
109
|
+
const order = [];
|
|
110
|
+
const task1 = async () => { await sleep(20); order.push(1); return 'a'; };
|
|
111
|
+
const task2 = async () => { await sleep(10); order.push(2); return 'b'; };
|
|
112
|
+
|
|
113
|
+
const results = await sequential([task1, task2]);
|
|
114
|
+
|
|
115
|
+
expect(order).toEqual([1, 2]);
|
|
116
|
+
expect(results).toEqual(['a', 'b']);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('isValidRegex', () => {
|
|
121
|
+
it('should validate correct regex strings', () => {
|
|
122
|
+
expect(isValidRegex('/hello/g')).toBe(true);
|
|
123
|
+
expect(isValidRegex('#[0-9]+#')).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should invalidate incorrect regex strings', () => {
|
|
127
|
+
expect(isValidRegex('/[a-z/')).toBe(false); // Unmatched bracket
|
|
128
|
+
expect(isValidRegex('hello')).toBe(false); // No delimiters
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('escapeHtml', () => {
|
|
133
|
+
it('should escape HTML tags and dangerous protocols', () => {
|
|
134
|
+
const input = '<script>alert("xss")</script><a href="javascript:void(0)">link</a>'; // Le protocole "javascript:" sera supprimé
|
|
135
|
+
const expected = '<script>alert("xss")</script><a href="void(0)">link</a>';
|
|
136
|
+
expect(escapeHtml(input)).toBe(expected);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('isUnsecureKey', () => {
|
|
141
|
+
it('should identify unsecure keys', () => {
|
|
142
|
+
expect(isUnsecureKey('__proto__')).toBe(true);
|
|
143
|
+
expect(isUnsecureKey('constructor')).toBe(true);
|
|
144
|
+
expect(isUnsecureKey('prototype')).toBe(true);
|
|
145
|
+
expect(isUnsecureKey('safeKey')).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('parseSafeJSON', () => {
|
|
150
|
+
it('should parse valid JSON', () => {
|
|
151
|
+
expect(parseSafeJSON('{"a":1}')).toEqual({ a: 1 });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should prevent prototype pollution', () => {
|
|
155
|
+
const polluted = parseSafeJSON('{"__proto__":{"polluted":true}}');
|
|
156
|
+
expect(polluted.polluted).toBeUndefined();
|
|
157
|
+
const obj = {};
|
|
158
|
+
expect(obj.polluted).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('isDate', () => {
|
|
163
|
+
it('should return true for valid date strings and objects', () => {
|
|
164
|
+
expect(isDate(new Date())).toBe(true);
|
|
165
|
+
expect(isDate('2023-01-01')).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should return false for invalid dates', () => {
|
|
169
|
+
expect(isDate('not a date')).toBe(false);
|
|
170
|
+
expect(isDate(null)).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('safeAssignObject', () => {
|
|
175
|
+
it('should assign property for a safe key', () => {
|
|
176
|
+
const obj = {};
|
|
177
|
+
safeAssignObject(obj, 'a', 1);
|
|
178
|
+
expect(obj.a).toBe(1);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should not assign property for an unsecure key', () => {
|
|
182
|
+
const obj = {};
|
|
183
|
+
safeAssignObject(obj, '__proto__', { polluted: true });
|
|
184
|
+
expect(obj.polluted).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('debounce', () => {
|
|
189
|
+
beforeEach(() => { vi.useFakeTimers(); });
|
|
190
|
+
afterEach(() => { vi.useRealTimers(); });
|
|
191
|
+
|
|
192
|
+
it('should call the function only once after the delay', () => {
|
|
193
|
+
const spy = vi.fn();
|
|
194
|
+
const debounced = debounce(spy, 500);
|
|
195
|
+
|
|
196
|
+
debounced();
|
|
197
|
+
debounced();
|
|
198
|
+
debounced();
|
|
199
|
+
|
|
200
|
+
vi.advanceTimersByTime(500);
|
|
201
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('uuidv4', () => {
|
|
206
|
+
it('should generate a valid v4 UUID', () => {
|
|
207
|
+
const uuid = uuidv4();
|
|
208
|
+
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
209
|
+
expect(uuid).toMatch(uuidV4Regex);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('cssProps', () => {
|
|
214
|
+
it('should convert a CSS string to a style object', () => {
|
|
215
|
+
const css = 'background-color: red; font-size: 16px;';
|
|
216
|
+
const expected = { backgroundColor: 'red', fontSize: '16px' };
|
|
217
|
+
expect(cssProps(css)).toEqual(expected);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('wordWrap', () => {
|
|
222
|
+
it('should wrap a long string at a specified width', () => {
|
|
223
|
+
const str = 'this is a long string to be wrapped';
|
|
224
|
+
const wrapped = wordWrap(str, 10);
|
|
225
|
+
expect(wrapped).toBe('this is a\nlong\nstring to\nbe wrapped');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('isPathRelativeTo', () => {
|
|
230
|
+
it('should correctly identify relative paths', () => {
|
|
231
|
+
expect(isPathRelativeTo('/a/b/c', '/a/b')).toBe(true);
|
|
232
|
+
expect(isPathRelativeTo('/a/d', '/a/b')).toBe(false);
|
|
233
|
+
expect(isPathRelativeTo('/a/b', '/a/b/c')).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('isGUID', () => {
|
|
238
|
+
it('should validate correct GUIDs', () => {
|
|
239
|
+
expect(isGUID('123e4567-e89b-12d3-a456-426614174000')).toBe(true);
|
|
240
|
+
});
|
|
241
|
+
it('should invalidate incorrect GUIDs', () => {
|
|
242
|
+
expect(isGUID('not-a-guid')).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('shuffle', () => {
|
|
247
|
+
it('should contain the same elements after shuffling', () => {
|
|
248
|
+
const original = [1, 2, 3, 4, 5];
|
|
249
|
+
const shuffled = [...original];
|
|
250
|
+
shuffle(shuffled);
|
|
251
|
+
expect(shuffled).toHaveLength(original.length);
|
|
252
|
+
expect(shuffled.sort()).toEqual(original.sort());
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('Seeded Random', () => {
|
|
257
|
+
it('should produce consistent random numbers for the same seed', () => {
|
|
258
|
+
setSeed(12345);
|
|
259
|
+
const rand1 = getRand();
|
|
260
|
+
const rand2 = getRand();
|
|
261
|
+
|
|
262
|
+
setSeed(12345);
|
|
263
|
+
const rand3 = getRand();
|
|
264
|
+
const rand4 = getRand();
|
|
265
|
+
|
|
266
|
+
expect(rand1).toBe(rand3);
|
|
267
|
+
expect(rand2).toBe(rand4);
|
|
268
|
+
expect(rand1).not.toBe(rand2);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('isLightColor', () => {
|
|
273
|
+
it('should identify light and dark colors', () => {
|
|
274
|
+
expect(isLightColor('#FFFFFF')).toBe(true);
|
|
275
|
+
expect(isLightColor('#000000')).toBe(false);
|
|
276
|
+
expect(isLightColor('#f0f0f0')).toBe(true);
|
|
277
|
+
expect(isLightColor('#3498db')).toBe(false); // primary-color from scss
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('tryParseJson', () => {
|
|
282
|
+
it('should parse valid JSON strings', () => {
|
|
283
|
+
expect(tryParseJson('{"a": 1}')).toEqual({ a: 1 });
|
|
284
|
+
});
|
|
285
|
+
it('should return null for invalid JSON strings', () => {
|
|
286
|
+
expect(tryParseJson('{"a": 1')).toBeNull();
|
|
287
|
+
expect(tryParseJson('not json')).toBeNull();
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('isIsoDate', () => {
|
|
292
|
+
it('should validate correct ISO date strings', () => {
|
|
293
|
+
expect(isIsoDate('2023-10-26T10:00:00.000Z')).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
it('should invalidate incorrect ISO date strings', () => {
|
|
296
|
+
expect(isIsoDate('2023-10-26')).toBe(false);
|
|
297
|
+
expect(isIsoDate(new Date().toString())).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('event system', () => {
|
|
302
|
+
beforeEach(() => {
|
|
303
|
+
event_off('test');
|
|
304
|
+
});
|
|
305
|
+
it('should trigger a registered event', () => {
|
|
306
|
+
const spy = vi.fn();
|
|
307
|
+
event_on('test', spy);
|
|
308
|
+
event_trigger('test', 'arg1');
|
|
309
|
+
expect(spy).toHaveBeenCalledWith('arg1');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('getFileExtension', () => {
|
|
314
|
+
it('should return the file extension', () => {
|
|
315
|
+
expect(getFileExtension('file.txt')).toBe('txt');
|
|
316
|
+
expect(getFileExtension('archive.tar.gz')).toBe('gz');
|
|
317
|
+
expect(getFileExtension('noextension')).toBe('');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('object_equals', () => {
|
|
322
|
+
it('should correctly compare objects', () => {
|
|
323
|
+
expect(object_equals({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } })).toBe(true);
|
|
324
|
+
expect(object_equals({ a: 1 }, { a: 2 })).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('stringToHslColor', () => {
|
|
329
|
+
it('should generate a consistent HSL color string', () => {
|
|
330
|
+
expect(stringToHslColor('test')).toBe('hsl(58, 70%, 55%)');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('countKeys', () => {
|
|
335
|
+
it('should count keys in nested objects and arrays', () => {
|
|
336
|
+
const obj = { a: 1, b: { c: 2, d: [1, { e: 3 }] } };
|
|
337
|
+
expect(countKeys(obj)).toBe(5);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
});
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
insertData,
|
|
10
10
|
editData,
|
|
11
11
|
deleteData,
|
|
12
|
-
searchData, installPack, deleteModels, createModel, patchData
|
|
12
|
+
searchData, installPack, deleteModels, createModel, patchData, datasCollection
|
|
13
13
|
} from '../src/index.js';
|
|
14
14
|
|
|
15
15
|
import {
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
getCollection,
|
|
18
18
|
getCollectionForUser as getAppUserCollection, getCollectionForUser
|
|
19
19
|
} from '../src/modules/mongodb.js';
|
|
20
|
-
import {
|
|
20
|
+
import { isConditionMet } from '../src/filter.js';
|
|
21
|
+
import {getRandom, isPlainObject} from "../src/core.js";
|
|
21
22
|
import {generateUniqueName, initEngine} from "../src/setenv.js";
|
|
22
23
|
import {purgeData} from "../src/modules/data/data.history.js";
|
|
23
24
|
import {removeFile} from "../src/modules/file.js";
|
|
@@ -1279,4 +1280,138 @@ describe('Data integration tests (CRUD, validation...)', () => {
|
|
|
1279
1280
|
expect(data[0]._id.toString()).toBe(noEnvDocId.toString());
|
|
1280
1281
|
});
|
|
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 où `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
|
+
});
|
|
1282
1417
|
});
|
package/test/user.test.js
CHANGED
|
@@ -16,34 +16,39 @@ describe('User Permission System with Filters', () => {
|
|
|
16
16
|
let permNoFilter, permWithSimpleFilter, permWithUserFilter, permWithReqFilter, permOrderEditForManager;
|
|
17
17
|
let roleWithPerms;
|
|
18
18
|
|
|
19
|
+
// Le parent qui possède les définitions de rôles et permissions
|
|
20
|
+
const parentUser = { username: generateUniqueName('perm_parent_user') };
|
|
21
|
+
|
|
19
22
|
beforeAll(async () => {
|
|
20
23
|
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow", "user", "assistant"]);
|
|
21
24
|
engine = await initEngine();
|
|
22
25
|
logger = engine.getComponent(Logger);
|
|
23
26
|
await onInit(engine);
|
|
27
|
+
// La collection est celle du parent, car c'est lui qui stocke les définitions
|
|
28
|
+
collection = await getCollectionForUser(parentUser);
|
|
24
29
|
});
|
|
25
30
|
|
|
26
31
|
beforeEach(async () => {
|
|
27
32
|
// Utiliser un utilisateur unique pour l'isolation des tests
|
|
28
33
|
const username = generateUniqueName('perm_test_user');
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
// L'utilisateur de test est un "enfant" de parentUser
|
|
35
|
+
testUser = { _id: new ObjectId(), username: username, _user: parentUser.username, roles: [], _model: 'user' };
|
|
36
|
+
testUserWithRole = { _id: new ObjectId(), username: username, _user: parentUser.username, roles: [], _model: 'user' };
|
|
31
37
|
systemUser = { roles: ['admin', 'product.view'] };
|
|
32
38
|
|
|
33
|
-
collection
|
|
34
|
-
await collection.deleteMany({ _user: username });
|
|
39
|
+
await collection.deleteMany({ _user: parentUser.username });
|
|
35
40
|
|
|
36
41
|
// --- Création des données de test ---
|
|
37
42
|
// Créer les modèles nécessaires pour les permissions et les rôles
|
|
38
|
-
await createModel({ name: 'permission', _user: username, fields: [{ name: 'name', type: 'string' }, { name: 'filter', type: 'code' }] });
|
|
39
|
-
await createModel({ name: 'role', _user: username, fields: [{ name: 'name', type: 'string' }, { name: 'permissions', type: 'relation', relation: 'permission', multiple: true }] });
|
|
40
|
-
await createModel({ name: 'userPermission', _user: username, fields: [{ name: 'user', type: 'relation', relation: 'user' }, { name: 'permission', type: 'relation', relation: 'permission' }, { name: 'isGranted', type: 'boolean' }, { name: 'filter', type: 'code' }] });
|
|
43
|
+
await createModel({ name: 'permission', description: '', _user: parentUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'filter', type: 'code' }] });
|
|
44
|
+
await createModel({ name: 'role', description: '', _user: parentUser.username, fields: [{ name: 'name', type: 'string' }, { name: 'permissions', type: 'relation', relation: 'permission', multiple: true }] });
|
|
45
|
+
await createModel({ name: 'userPermission', description: '', _user: parentUser.username, fields: [{ name: 'user', type: 'relation', relation: 'user' }, { name: 'permission', type: 'relation', relation: 'permission' }, { name: 'isGranted', type: 'boolean' }, { name: 'filter', type: 'code',language:'json' }] });
|
|
41
46
|
|
|
42
47
|
const permResult = await collection.insertMany([
|
|
43
|
-
{ _model: 'permission', name: 'product.view', _user: username },
|
|
44
|
-
{ _model: 'permission', name: 'order.edit', filter: { status: 'pending' }, _user: username },
|
|
45
|
-
{ _model: 'permission', name: 'document.edit', filter: { createdBy: '{user._id}' }, _user: username },
|
|
46
|
-
{ _model: 'permission', name: 'login.attempt', filter: { "ip": "{req.ip}" }, _user: username }
|
|
48
|
+
{ _model: 'permission', name: 'product.view', _user: parentUser.username },
|
|
49
|
+
{ _model: 'permission', name: 'order.edit', filter: { status: 'pending' }, _user: parentUser.username },
|
|
50
|
+
{ _model: 'permission', name: 'document.edit', filter: { createdBy: '{user._id}' }, _user: parentUser.username },
|
|
51
|
+
{ _model: 'permission', name: 'login.attempt', filter: { "ip": "{req.ip}" }, _user: parentUser.username }
|
|
47
52
|
]);
|
|
48
53
|
permNoFilter = permResult.insertedIds[0];
|
|
49
54
|
permWithSimpleFilter = permResult.insertedIds[1];
|
|
@@ -54,7 +59,7 @@ describe('User Permission System with Filters', () => {
|
|
|
54
59
|
_model: 'role',
|
|
55
60
|
name: 'Editor',
|
|
56
61
|
permissions: [permNoFilter.toString(), permWithSimpleFilter.toString()],
|
|
57
|
-
_user: username
|
|
62
|
+
_user: parentUser.username
|
|
58
63
|
});
|
|
59
64
|
roleWithPerms = roleResult.insertedId;
|
|
60
65
|
|
|
@@ -65,9 +70,9 @@ describe('User Permission System with Filters', () => {
|
|
|
65
70
|
afterEach(async () => {
|
|
66
71
|
// Nettoyer les données créées
|
|
67
72
|
if (collection) {
|
|
68
|
-
await collection.deleteMany({ _user:
|
|
73
|
+
await collection.deleteMany({ _user: parentUser.username });
|
|
69
74
|
}
|
|
70
|
-
await deleteModels({ _user:
|
|
75
|
+
await deleteModels({ _user: parentUser.username });
|
|
71
76
|
});
|
|
72
77
|
|
|
73
78
|
it('should return true for a permission without filter granted by a role', async () => {
|
|
@@ -91,7 +96,7 @@ describe('User Permission System with Filters', () => {
|
|
|
91
96
|
user: testUser._id.toString(),
|
|
92
97
|
permission: permWithUserFilter.toString(),
|
|
93
98
|
isGranted: true,
|
|
94
|
-
_user:
|
|
99
|
+
_user: parentUser.username
|
|
95
100
|
});
|
|
96
101
|
|
|
97
102
|
const result = await hasPermission('document.edit', testUser);
|
|
@@ -104,7 +109,7 @@ describe('User Permission System with Filters', () => {
|
|
|
104
109
|
user: testUserWithRole._id.toString(),
|
|
105
110
|
permission: permWithUserFilter.toString(),
|
|
106
111
|
isGranted: true,
|
|
107
|
-
_user:
|
|
112
|
+
_user: parentUser.username
|
|
108
113
|
});
|
|
109
114
|
|
|
110
115
|
const result = await hasPermission('document.edit', testUserWithRole);
|
|
@@ -117,7 +122,7 @@ describe('User Permission System with Filters', () => {
|
|
|
117
122
|
user: testUser._id.toString(),
|
|
118
123
|
permission: permWithReqFilter.toString(),
|
|
119
124
|
isGranted: true,
|
|
120
|
-
_user:
|
|
125
|
+
_user: parentUser.username
|
|
121
126
|
});
|
|
122
127
|
|
|
123
128
|
const mockReq = {
|
|
@@ -134,14 +139,14 @@ describe('User Permission System with Filters', () => {
|
|
|
134
139
|
it('should revoke a permission from a role via userPermission exception', async () => {
|
|
135
140
|
let result = await hasPermission('order.edit', testUserWithRole);
|
|
136
141
|
expect(result).toEqual({ status: 'pending' });
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
|
|
143
|
+
// Utiliser insertData pour déclencher l'invalidation du cache
|
|
144
|
+
await insertData('userPermission', {
|
|
140
145
|
user: testUserWithRole._id.toString(),
|
|
141
146
|
permission: permWithSimpleFilter.toString(),
|
|
142
147
|
isGranted: false,
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
}, {}, parentUser, true, true, false // le dernier false pour bypasser la vérification de permission pour le test
|
|
149
|
+
);
|
|
145
150
|
|
|
146
151
|
result = await hasPermission('order.edit', testUserWithRole);
|
|
147
152
|
expect(result).toBe(false);
|
|
@@ -152,7 +157,7 @@ describe('User Permission System with Filters', () => {
|
|
|
152
157
|
_model: 'permission',
|
|
153
158
|
name: 'order.edit',
|
|
154
159
|
filter: { manager_id: '{user._id}' },
|
|
155
|
-
_user:
|
|
160
|
+
_user: parentUser.username
|
|
156
161
|
});
|
|
157
162
|
permOrderEditForManager = permResult.insertedId;
|
|
158
163
|
|
|
@@ -161,7 +166,7 @@ describe('User Permission System with Filters', () => {
|
|
|
161
166
|
user: testUserWithRole._id.toString(),
|
|
162
167
|
permission: permOrderEditForManager.toString(),
|
|
163
168
|
isGranted: true,
|
|
164
|
-
_user:
|
|
169
|
+
_user: parentUser.username
|
|
165
170
|
});
|
|
166
171
|
|
|
167
172
|
const result = await hasPermission('order.edit', testUserWithRole);
|
|
@@ -175,14 +180,13 @@ describe('User Permission System with Filters', () => {
|
|
|
175
180
|
expect(result).toEqual({ status: 'pending' });
|
|
176
181
|
|
|
177
182
|
// On ajoute une exception qui accorde la même permission mais avec un filtre différent
|
|
178
|
-
|
|
179
|
-
|
|
183
|
+
// Utiliser insertData pour s'assurer que le cache est invalidé
|
|
184
|
+
await insertData('userPermission', {
|
|
180
185
|
user: testUserWithRole._id.toString(),
|
|
181
186
|
permission: permWithSimpleFilter.toString(), // La permission 'order.edit'
|
|
182
187
|
isGranted: true,
|
|
183
188
|
filter: { "assignedTo": "{user._id}" }, // Le nouveau filtre
|
|
184
|
-
|
|
185
|
-
});
|
|
189
|
+
}, {}, parentUser, true, true, false);
|
|
186
190
|
|
|
187
191
|
// La fonction doit maintenant retourner le filtre de l'exception
|
|
188
192
|
result = await hasPermission('order.edit', testUserWithRole);
|
|
@@ -198,7 +202,7 @@ describe('User Permission System with Filters', () => {
|
|
|
198
202
|
permission: permWithUserFilter.toString(), // La permission 'document.edit'
|
|
199
203
|
isGranted: true,
|
|
200
204
|
// Pas de champ 'filter' ici
|
|
201
|
-
_user:
|
|
205
|
+
_user: parentUser.username
|
|
202
206
|
});
|
|
203
207
|
|
|
204
208
|
// La fonction doit retourner le filtre de la permission de base.
|