data-primals-engine 1.6.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -900
- package/client/index.js +3 -0
- package/client/package-lock.json +7563 -8811
- package/client/package.json +11 -1
- package/client/src/App.scss +29 -0
- package/client/src/AssistantChat.jsx +369 -362
- package/client/src/DataEditor.jsx +383 -383
- package/client/src/DataLayout.jsx +54 -20
- package/client/src/ModelList.jsx +280 -280
- package/client/src/ViewSwitcher.scss +0 -31
- package/client/src/_variables.scss +3 -0
- package/client/src/constants.js +81 -100
- package/client/src/contexts/CommandContext.jsx +274 -259
- package/client/vite.config.js +30 -30
- package/doc/AI-assistance.md +93 -0
- package/doc/Advanced-workflows.md +90 -0
- package/doc/Event-system.md +79 -0
- package/doc/Packs-gallery.md +73 -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 +20 -10
- package/src/client.js +6 -4
- package/src/constants.js +1 -1
- package/src/core.js +31 -12
- package/src/defaultModels.js +1 -1
- package/src/engine.js +342 -335
- package/src/filter.js +72 -173
- package/src/migrate.js +1 -1
- package/src/modules/assistant/assistant.js +30 -20
- package/src/modules/data/data.backup.js +4 -4
- package/src/modules/data/data.js +8 -7
- package/src/modules/data/data.operations.js +186 -133
- package/src/modules/data/data.relations.js +3 -2
- package/src/modules/data/data.scheduling.js +1 -1
- package/src/modules/mongodb.js +17 -8
- package/src/modules/swagger.js +25 -5
- package/src/modules/user.js +108 -79
- package/src/modules/workflow.js +138 -3
- package/src/packs.js +5697 -5697
- package/src/profiles.js +19 -0
- package/swagger-en.yml +3391 -1550
- package/swagger-fr.yml +3385 -2896
- package/test/assistant.test.js +2 -2
- package/test/core.test.js +341 -0
- package/test/data.backup.integration.test.js +3 -1
- package/test/data.history.integration.test.js +0 -1
- package/test/data.integration.test.js +137 -2
- package/test/user.test.js +33 -29
package/test/assistant.test.js
CHANGED
|
@@ -168,7 +168,7 @@ describe('Assistant Module Unit Tests', () => {
|
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
it('should correct a malformed filter from the AI', async () => {
|
|
171
|
-
const aiResponse = JSON.stringify({
|
|
171
|
+
const aiResponse = JSON.stringify([{
|
|
172
172
|
action: 'search',
|
|
173
173
|
params: {
|
|
174
174
|
model: modelName,
|
|
@@ -178,7 +178,7 @@ describe('Assistant Module Unit Tests', () => {
|
|
|
178
178
|
"$lt": ["$price", 200]
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
-
});
|
|
181
|
+
}]);
|
|
182
182
|
const mockLLM = {
|
|
183
183
|
stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
|
|
184
184
|
};
|
|
@@ -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
|
+
});
|
|
@@ -57,7 +57,7 @@ let engineInstance;
|
|
|
57
57
|
let testDatasApi;
|
|
58
58
|
|
|
59
59
|
const backupDir = path.resolve('./test-backups'); // Use an absolute path
|
|
60
|
-
|
|
60
|
+
let engine;
|
|
61
61
|
beforeAll(async () => {
|
|
62
62
|
|
|
63
63
|
process.env.BACKUP_DIR = backupDir; // Set backup directory
|
|
@@ -66,6 +66,8 @@ beforeAll(async () => {
|
|
|
66
66
|
if (!fs.existsSync(backupDir)) {
|
|
67
67
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
68
68
|
}
|
|
69
|
+
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow", "user", "assistant"]);
|
|
70
|
+
engine = await initEngine();
|
|
69
71
|
|
|
70
72
|
// Delete any existing files in the backup directory
|
|
71
73
|
fs.readdirSync(backupDir).forEach(file => {
|
|
@@ -8,7 +8,6 @@ import { insertData, editData, deleteData } from '../src/index.js';
|
|
|
8
8
|
import { getCollection, getCollectionForUser } from '../src/modules/mongodb.js';
|
|
9
9
|
import { generateUniqueName, initEngine } from "../src/setenv.js";
|
|
10
10
|
import {purgeData} from "../src/modules/data/data.history.js";
|
|
11
|
-
import {MongoDatabase} from "../src/engine.js";
|
|
12
11
|
|
|
13
12
|
let engine;
|
|
14
13
|
let testUser;
|
|
@@ -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
|
});
|