data-primals-engine 1.2.0 → 1.2.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.
- package/README.md +78 -14
- package/client/src/App.scss +11 -6
- package/client/src/ConditionBuilder2.jsx +47 -6
- package/client/src/DataLayout.jsx +4 -21
- package/client/src/DataTable.jsx +3 -1
- package/client/src/Field.jsx +1 -1
- package/client/src/ModelCreator.jsx +4 -4
- package/client/src/ModelCreator.scss +10 -0
- package/client/src/ModelCreatorField.jsx +34 -34
- package/client/src/RTE.jsx +2 -1
- package/client/src/RestoreConfirmationModal.jsx +0 -137
- package/client/src/filter.js +2 -0
- package/client/src/hooks/useTutorials.jsx +3 -3
- package/package.json +4 -3
- package/src/core.js +6 -0
- package/src/data.js +4 -4
- package/src/defaultModels.js +33 -0
- package/src/email.js +2 -2
- package/src/engine.js +10 -7
- package/src/events.js +25 -3
- package/src/modules/bucket.js +115 -5
- package/src/modules/data.js +166 -93
- package/src/modules/file.js +98 -57
- package/src/modules/test +147 -0
- package/src/modules/user.js +111 -56
- package/test/data.backup.integration.test.js +1 -1
- package/test/events.test.js +202 -0
- package/test/file.test.js +196 -0
- package/test/user.test.js +203 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// events.test.js
|
|
2
|
+
import { Event } from '../src/events.js';
|
|
3
|
+
import {vitest} from "vitest";
|
|
4
|
+
import {expect, describe, it, beforeEach, beforeAll, afterAll} from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('Event System', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
// Clear all events before each test to ensure a clean state
|
|
9
|
+
for (const system in Event) {
|
|
10
|
+
if (Event.hasOwnProperty(system) && typeof Event[system] === 'object') {
|
|
11
|
+
for (const eventName in Event[system]) {
|
|
12
|
+
if (Event[system].hasOwnProperty(eventName)) {
|
|
13
|
+
delete Event[system][eventName];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should allow listening for and triggering an event', () => {
|
|
21
|
+
const mockCallback = vitest.fn();
|
|
22
|
+
const eventName = 'testEvent';
|
|
23
|
+
|
|
24
|
+
Event.Listen(eventName, mockCallback);
|
|
25
|
+
Event.Trigger(eventName);
|
|
26
|
+
|
|
27
|
+
expect(mockCallback).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
it('should pass arguments to the event callback', () => {
|
|
30
|
+
const mockCallback = vitest.fn();
|
|
31
|
+
const eventName = 'testEventWithArgs';
|
|
32
|
+
const arg1 = 'hello';
|
|
33
|
+
const arg2 = 123;
|
|
34
|
+
|
|
35
|
+
Event.Listen(eventName, mockCallback, "priority", "medium");
|
|
36
|
+
Event.Trigger(eventName, "priority", "medium", arg1, arg2);
|
|
37
|
+
|
|
38
|
+
expect(mockCallback).toHaveBeenCalledTimes(1);
|
|
39
|
+
expect(mockCallback).toHaveBeenCalledWith(arg1, arg2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should allow removing a specific callback', () => {
|
|
43
|
+
const mockCallback1 = vitest.fn();
|
|
44
|
+
const mockCallback2 = vitest.fn();
|
|
45
|
+
const eventName = 'testEventRemoveCallback';
|
|
46
|
+
|
|
47
|
+
Event.Listen(eventName, mockCallback1);
|
|
48
|
+
Event.Listen(eventName, mockCallback2);
|
|
49
|
+
Event.RemoveCallback(eventName, mockCallback1);
|
|
50
|
+
Event.Trigger(eventName);
|
|
51
|
+
|
|
52
|
+
expect(mockCallback1).not.toHaveBeenCalled();
|
|
53
|
+
expect(mockCallback2).toHaveBeenCalledTimes(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should not trigger a callback if the event is not listened to', () => {
|
|
57
|
+
const mockCallback = vitest.fn();
|
|
58
|
+
const eventName = 'nonExistentEvent';
|
|
59
|
+
|
|
60
|
+
Event.Trigger(eventName);
|
|
61
|
+
|
|
62
|
+
expect(mockCallback).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle different systems and layers', () => {
|
|
66
|
+
const mockCallbackPriority = vitest.fn();
|
|
67
|
+
const mockCallbackLog = vitest.fn();
|
|
68
|
+
const eventName = 'testEventMultiSystem';
|
|
69
|
+
|
|
70
|
+
Event.Listen(eventName, mockCallbackPriority, 'priority', 'high');
|
|
71
|
+
Event.Listen(eventName, mockCallbackLog, 'log', 'info');
|
|
72
|
+
|
|
73
|
+
Event.Trigger(eventName, 'priority', 'high');
|
|
74
|
+
expect(mockCallbackPriority).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(mockCallbackLog).not.toHaveBeenCalled();
|
|
76
|
+
|
|
77
|
+
vitest.clearAllMocks(); // Reset mock counts
|
|
78
|
+
|
|
79
|
+
Event.Trigger(eventName, 'log', 'info');
|
|
80
|
+
expect(mockCallbackPriority).not.toHaveBeenCalled();
|
|
81
|
+
expect(mockCallbackLog).toHaveBeenCalledTimes(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should throw an error if an invalid system or layer is used with Listen', () => {
|
|
85
|
+
expect(() => {
|
|
86
|
+
Event.Listen('invalidEvent', () => {}, 'invalidSystem', 'invalidLayer');
|
|
87
|
+
}).toThrowError(/System 'invalidSystem' does not exist./);
|
|
88
|
+
|
|
89
|
+
expect(() => {
|
|
90
|
+
Event.Listen('invalidEvent', () => {}, 'priority', 'invalidLayer');
|
|
91
|
+
}).toThrowError(/Layer 'invalidLayer' does not exist/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Event.Trigger result merging', () => {
|
|
95
|
+
it('should merge objects from multiple callbacks using spread operator', () => {
|
|
96
|
+
const eventName = 'mergeObjectEvent';
|
|
97
|
+
const callback1 = vitest.fn().mockReturnValue({ a: 1, b: 2 });
|
|
98
|
+
const callback2 = vitest.fn().mockReturnValue({ b: 3, c: 4 }); // b will be overwritten
|
|
99
|
+
|
|
100
|
+
Event.Listen(eventName, callback1);
|
|
101
|
+
Event.Listen(eventName, callback2);
|
|
102
|
+
|
|
103
|
+
const result = Event.Trigger(eventName);
|
|
104
|
+
|
|
105
|
+
expect(result).toEqual({ a: 1, b: 3, c: 4 });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should concatenate arrays from multiple callbacks', () => {
|
|
109
|
+
const eventName = 'mergeArrayEvent';
|
|
110
|
+
const callback1 = vitest.fn().mockReturnValue([1, 2]);
|
|
111
|
+
const callback2 = vitest.fn().mockReturnValue([3, 4]);
|
|
112
|
+
|
|
113
|
+
Event.Listen(eventName, callback1);
|
|
114
|
+
Event.Listen(eventName, callback2);
|
|
115
|
+
|
|
116
|
+
const result = Event.Trigger(eventName);
|
|
117
|
+
|
|
118
|
+
expect(result).toEqual([1, 2, 3, 4]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should concatenate strings from multiple callbacks', () => {
|
|
122
|
+
const eventName = 'mergeStringEvent';
|
|
123
|
+
const callback1 = vitest.fn().mockReturnValue('Hello ');
|
|
124
|
+
const callback2 = vitest.fn().mockReturnValue('World');
|
|
125
|
+
|
|
126
|
+
Event.Listen(eventName, callback1);
|
|
127
|
+
Event.Listen(eventName, callback2);
|
|
128
|
+
|
|
129
|
+
const result = Event.Trigger(eventName);
|
|
130
|
+
|
|
131
|
+
expect(result).toBe('Hello World');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should add numbers from multiple callbacks', () => {
|
|
135
|
+
const eventName = 'mergeNumberEvent';
|
|
136
|
+
const callback1 = vitest.fn().mockReturnValue(10);
|
|
137
|
+
const callback2 = vitest.fn().mockReturnValue(20);
|
|
138
|
+
|
|
139
|
+
Event.Listen(eventName, callback1);
|
|
140
|
+
Event.Listen(eventName, callback2);
|
|
141
|
+
|
|
142
|
+
const result = Event.Trigger(eventName);
|
|
143
|
+
|
|
144
|
+
expect(result).toBe(30);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should perform a logical AND on booleans from multiple callbacks', () => {
|
|
148
|
+
const eventName = 'mergeBooleanEvent';
|
|
149
|
+
|
|
150
|
+
// Case 1: true && true -> true
|
|
151
|
+
const cb_true1 = vitest.fn().mockReturnValue(true);
|
|
152
|
+
const cb_true2 = vitest.fn().mockReturnValue(true);
|
|
153
|
+
Event.Listen(eventName, cb_true1);
|
|
154
|
+
Event.Listen(eventName, cb_true2);
|
|
155
|
+
expect(Event.Trigger(eventName)).toBe(true);
|
|
156
|
+
Event.RemoveAllCallbacks(eventName); // Clean up for next case
|
|
157
|
+
|
|
158
|
+
// Case 2: true && false -> false
|
|
159
|
+
const cb_false1 = vitest.fn().mockReturnValue(false);
|
|
160
|
+
Event.Listen(eventName, cb_true1);
|
|
161
|
+
Event.Listen(eventName, cb_false1);
|
|
162
|
+
expect(Event.Trigger(eventName)).toBe(false);
|
|
163
|
+
Event.RemoveAllCallbacks(eventName);
|
|
164
|
+
|
|
165
|
+
// Case 3: false && true -> false
|
|
166
|
+
Event.Listen(eventName, cb_false1);
|
|
167
|
+
Event.Listen(eventName, cb_true1);
|
|
168
|
+
expect(Event.Trigger(eventName)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle mixed-type return values', () => {
|
|
172
|
+
const eventName = 'mergeMixedEvent';
|
|
173
|
+
const callback1 = vitest.fn().mockReturnValue(100); // number
|
|
174
|
+
const callback2 = vitest.fn().mockReturnValue({ a: 1 }); // object
|
|
175
|
+
|
|
176
|
+
Event.Listen(eventName, callback1);
|
|
177
|
+
Event.Listen(eventName, callback2);
|
|
178
|
+
|
|
179
|
+
const result = Event.Trigger(eventName);
|
|
180
|
+
|
|
181
|
+
// The logic initializes `ret` with the first value (100).
|
|
182
|
+
// When the second callback returns an object, it re-initializes `ret` to {}
|
|
183
|
+
// and merges the object. The final result is the object.
|
|
184
|
+
expect(result).toEqual({ a: 1 });
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should ignore null and undefined return values while merging', () => {
|
|
188
|
+
const eventName = 'ignoreNullsEvent';
|
|
189
|
+
const callback1 = vitest.fn().mockReturnValue(null);
|
|
190
|
+
const callback2 = vitest.fn().mockReturnValue({ data: 'important' });
|
|
191
|
+
const callback3 = vitest.fn().mockReturnValue(undefined);
|
|
192
|
+
|
|
193
|
+
Event.Listen(eventName, callback1);
|
|
194
|
+
Event.Listen(eventName, callback2);
|
|
195
|
+
Event.Listen(eventName, callback3);
|
|
196
|
+
|
|
197
|
+
const result = Event.Trigger(eventName);
|
|
198
|
+
|
|
199
|
+
expect(result).toEqual({ data: 'important' });
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// __tests__/file.integration.test.js
|
|
2
|
+
import { expect, describe, it, beforeEach, afterEach, beforeAll, afterAll, vi } from 'vitest';
|
|
3
|
+
import { Config } from '../src/config.js';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { ObjectId } from 'mongodb';
|
|
8
|
+
import { initEngine, generateUniqueName } from "../src/setenv.js";
|
|
9
|
+
import { addFile, removeFile, getFile, onInit } from "../src/modules/file.js";
|
|
10
|
+
import { getCollection } from "../src/modules/mongodb.js";
|
|
11
|
+
import { Logger } from "../src/gameObject.js";
|
|
12
|
+
import {maxPrivateFileSize} from "../src/constants.js";
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
vi.stubEnv('ENCRYPTION_KEY', '12345678901234567890123456789012');
|
|
17
|
+
|
|
18
|
+
// Mock pour les fichiers uploadés
|
|
19
|
+
const createMockFile = (name, size, type = 'text/plain') => {
|
|
20
|
+
const tempPath = path.join(__dirname, 'temp', name);
|
|
21
|
+
fs.writeFileSync(tempPath, 'a'.repeat(size));
|
|
22
|
+
return {
|
|
23
|
+
name,
|
|
24
|
+
size,
|
|
25
|
+
type,
|
|
26
|
+
path: tempPath
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
let engine;
|
|
30
|
+
const testUploadDir = path.join(process.cwd(), "uploads", "private");
|
|
31
|
+
const testTempDir = path.join(__dirname, 'temp');
|
|
32
|
+
let filesCollection;
|
|
33
|
+
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
Config.Set("modules", ["mongodb", "data", "file", "bucket", "user","file"]);
|
|
36
|
+
engine= await initEngine();
|
|
37
|
+
await onInit(engine);
|
|
38
|
+
|
|
39
|
+
// Créer les répertoires nécessaires
|
|
40
|
+
if (!fs.existsSync(testUploadDir)) {
|
|
41
|
+
fs.mkdirSync(testUploadDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
if (!fs.existsSync(testTempDir)) {
|
|
44
|
+
fs.mkdirSync(testTempDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
filesCollection = await getCollection("files");
|
|
48
|
+
});
|
|
49
|
+
describe('File Module Integration Tests', () => {
|
|
50
|
+
let testUser;
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
// Créer un utilisateur de test
|
|
55
|
+
testUser = {
|
|
56
|
+
username: generateUniqueName('testuser'),
|
|
57
|
+
_user: generateUniqueName('testuser'),
|
|
58
|
+
userPlan: 'premium',
|
|
59
|
+
permissions: ['API_UPLOAD_FILE']
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Nettoyer les collections avant chaque test
|
|
63
|
+
await filesCollection.deleteMany({});
|
|
64
|
+
|
|
65
|
+
// Nettoyer le répertoire upload
|
|
66
|
+
const files = fs.readdirSync(testUploadDir);
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
fs.unlinkSync(path.join(testUploadDir, file));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterAll(async () => {
|
|
73
|
+
// Nettoyer après tous les tests
|
|
74
|
+
await filesCollection.deleteMany({});
|
|
75
|
+
if (fs.existsSync(testUploadDir)) {
|
|
76
|
+
fs.rmSync(testUploadDir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
if (fs.existsSync(testTempDir)) {
|
|
79
|
+
fs.rmSync(testTempDir, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('addFile', () => {
|
|
84
|
+
it('should successfully add a file with local storage', async () => {
|
|
85
|
+
const mockFile = createMockFile('test.txt', 1024); // 1KB file
|
|
86
|
+
|
|
87
|
+
const guid = await addFile(mockFile, testUser);
|
|
88
|
+
|
|
89
|
+
// Vérifier le retour
|
|
90
|
+
expect(guid).toBeDefined();
|
|
91
|
+
expect(guid).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
|
|
92
|
+
|
|
93
|
+
// Vérifier en base de données
|
|
94
|
+
const fileRecord = await filesCollection.findOne({ guid });
|
|
95
|
+
expect(fileRecord).toBeDefined();
|
|
96
|
+
expect(fileRecord.filename).toBeDefined();
|
|
97
|
+
expect(fileRecord.size).toBe(1024);
|
|
98
|
+
expect(fileRecord.user).toBe(testUser.username);
|
|
99
|
+
expect(fileRecord.storage).toBe('local');
|
|
100
|
+
|
|
101
|
+
// Vérifier que le fichier existe physiquement
|
|
102
|
+
const expectedPath = path.join(testUploadDir, `${guid}.txt`);
|
|
103
|
+
expect(fs.existsSync(expectedPath)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should throw error when file size exceeds limit', async () => {
|
|
107
|
+
const mockFile = createMockFile('large.txt', maxPrivateFileSize+1); // 11MB (dépasse la limite de 10MB)
|
|
108
|
+
|
|
109
|
+
await expect(addFile(mockFile, testUser)).rejects.toThrow(/La taille du fichier dépasse la limite autorisée/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should throw error when storage limit exceeded', async ({skip}) => {
|
|
113
|
+
skip();
|
|
114
|
+
// Mock la fonction de calcul d'usage pour simuler un dépassement
|
|
115
|
+
vi.spyOn(engine.userProvider, 'getUserStorageLimit').mockResolvedValue(85 * 1024 * 1024); // 5MB
|
|
116
|
+
|
|
117
|
+
const mockFile = createMockFile('test.txt', 1024 * 1024); // 1MB
|
|
118
|
+
|
|
119
|
+
await expect(addFile(mockFile, testUser)).rejects.toThrow("api.data.storageLimitExceeded");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should throw error when server capacity is insufficient', async ({skip}) => {
|
|
123
|
+
skip();
|
|
124
|
+
// Mock la vérification de capacité serveur
|
|
125
|
+
vi.spyOn(engine.userProvider, 'checkServerCapacity').mockResolvedValue({ isSufficient: false });
|
|
126
|
+
|
|
127
|
+
const mockFile = createMockFile('test.txt', 1024);
|
|
128
|
+
|
|
129
|
+
await expect(addFile(mockFile, testUser)).rejects.toThrow("api.data.serverStorageFull");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('removeFile', () => {
|
|
134
|
+
it('should successfully remove a locally stored file', async () => {
|
|
135
|
+
// D'abord ajouter un fichier
|
|
136
|
+
const mockFile = createMockFile('to-delete.txt', 1024);
|
|
137
|
+
const guid = await addFile(mockFile, testUser);
|
|
138
|
+
|
|
139
|
+
// Vérifier qu'il existe avant suppression
|
|
140
|
+
const beforeDelete = await getFile(guid);
|
|
141
|
+
expect(beforeDelete).toBeDefined();
|
|
142
|
+
|
|
143
|
+
// Supprimer le fichier
|
|
144
|
+
await removeFile(guid, testUser);
|
|
145
|
+
|
|
146
|
+
// Vérifier en base de données
|
|
147
|
+
const afterDelete = await getFile(guid);
|
|
148
|
+
expect(afterDelete).toBeNull();
|
|
149
|
+
|
|
150
|
+
// Vérifier que le fichier physique a été supprimé
|
|
151
|
+
const expectedPath = path.join(testUploadDir, `${guid}.txt`);
|
|
152
|
+
expect(fs.existsSync(expectedPath)).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should handle trying to remove non-existent file gracefully', async () => {
|
|
156
|
+
const nonExistentGuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
157
|
+
|
|
158
|
+
vi.spyOn(engine.getComponent(Logger), "info");
|
|
159
|
+
|
|
160
|
+
// Cela ne devrait pas throw d'erreur
|
|
161
|
+
await removeFile(nonExistentGuid, testUser);
|
|
162
|
+
|
|
163
|
+
// Vérifier que le logger a bien enregistré l'info
|
|
164
|
+
expect(engine.getComponent(Logger).info).toHaveBeenCalledWith(
|
|
165
|
+
expect.stringContaining(`Tentative de suppression d'un fichier inexistant`),
|
|
166
|
+
'warn'
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should throw error when GUID is invalid', async () => {
|
|
171
|
+
await expect(removeFile('invalid-guid', testUser)).rejects.toThrow("Le GUID du fichier n'est pas valide.");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('getFile', () => {
|
|
176
|
+
it('should retrieve file metadata', async () => {
|
|
177
|
+
const mockFile = createMockFile('metadata-test.txt', 2048);
|
|
178
|
+
const guid = await addFile(mockFile, testUser);
|
|
179
|
+
|
|
180
|
+
const fileData = await getFile(guid);
|
|
181
|
+
|
|
182
|
+
expect(fileData).toBeDefined();
|
|
183
|
+
expect(fileData.guid).toBe(guid);
|
|
184
|
+
expect(fileData.filename).toContain('.txt');
|
|
185
|
+
expect(fileData.size).toBe(2048);
|
|
186
|
+
expect(fileData.user).toBe(testUser.username);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return null for non-existent file', async () => {
|
|
190
|
+
const nonExistentGuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
191
|
+
const fileData = await getFile(nonExistentGuid);
|
|
192
|
+
|
|
193
|
+
expect(fileData).toBeNull();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { expect, describe, it, beforeEach, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { hasPermission, getUserActivePermissions } from '../src/modules/user.js';
|
|
3
|
+
import {initEngine} from "data-primals-engine/setenv";
|
|
4
|
+
import {generateUniqueName} from "../src/setenv.js";
|
|
5
|
+
import {
|
|
6
|
+
getCollectionForUser as getAppUserCollection,
|
|
7
|
+
modelsCollection as getAppModelsCollection
|
|
8
|
+
} from "data-primals-engine/modules/mongodb";
|
|
9
|
+
import {Config} from "../src/index.js";
|
|
10
|
+
let permRead, permWrite, permDelete, permManage;
|
|
11
|
+
let roleEditor;
|
|
12
|
+
let testUser, adminUser, roleViewer;
|
|
13
|
+
let currentTestUser;
|
|
14
|
+
let testModelsColInstance, testDatasColInstance;
|
|
15
|
+
// Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
|
|
16
|
+
async function setupTestContext() {
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
// Créer un utilisateur unique pour ce test
|
|
20
|
+
const username = generateUniqueName('testuserDataIntegration');
|
|
21
|
+
currentTestUser = {
|
|
22
|
+
username,
|
|
23
|
+
userPlan: 'free',
|
|
24
|
+
_model: 'user',
|
|
25
|
+
_user: username,
|
|
26
|
+
email: generateUniqueName('test') + '@example.com'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Initialize collection instances after the engine is ready
|
|
30
|
+
testModelsColInstance = getAppModelsCollection;
|
|
31
|
+
testDatasColInstance = await getAppUserCollection(currentTestUser);
|
|
32
|
+
|
|
33
|
+
await testDatasColInstance.deleteMany({}); // Nettoyage de la base de test
|
|
34
|
+
// 1. Créer les permissions
|
|
35
|
+
const permissions = await testDatasColInstance.insertMany([
|
|
36
|
+
{ _model: 'permission', name: 'post:read', description: 'Lire les articles' },
|
|
37
|
+
{ _model: 'permission', name: 'post:write', description: 'Écrire des articles' },
|
|
38
|
+
{ _model: 'permission', name: 'post:delete', description: 'Supprimer des articles' },
|
|
39
|
+
{ _model: 'permission', name: 'user:manage', description: 'Gérer les utilisateurs' }
|
|
40
|
+
]);
|
|
41
|
+
permRead = permissions.insertedIds[0];
|
|
42
|
+
permWrite = permissions.insertedIds[1];
|
|
43
|
+
permDelete = permissions.insertedIds[2];
|
|
44
|
+
permManage = permissions.insertedIds[3];
|
|
45
|
+
|
|
46
|
+
// 2. Créer les rôles
|
|
47
|
+
const roles = await testDatasColInstance.insertMany([
|
|
48
|
+
{ _user: currentTestUser.username, _model: 'role', name: 'Viewer', permissions: [permRead.toString()] },
|
|
49
|
+
{ _user: currentTestUser.username, _model: 'role', name: 'Editor', permissions: [permRead.toString(), permWrite.toString()] }
|
|
50
|
+
]);
|
|
51
|
+
roleViewer = roles.insertedIds[0];
|
|
52
|
+
roleEditor = roles.insertedIds[1];
|
|
53
|
+
|
|
54
|
+
// 3. Créer les utilisateurs
|
|
55
|
+
const users = await testDatasColInstance.insertMany([
|
|
56
|
+
{ _user: currentTestUser.username, _model: 'user', roles: [roleEditor.toString()] },
|
|
57
|
+
{ _user: currentTestUser.username, _model: 'user', roles: [roleEditor.toString()] }
|
|
58
|
+
]);
|
|
59
|
+
testUser = {...currentTestUser, _id: users.insertedIds[0].toString(), roles: [roleEditor.toString()] };
|
|
60
|
+
adminUser = {...currentTestUser, _id: users.insertedIds[1].toString(), roles: [roleEditor.toString()] };
|
|
61
|
+
};
|
|
62
|
+
let engine;
|
|
63
|
+
describe('User Permission Logic', () => {
|
|
64
|
+
|
|
65
|
+
// --- SETUP : Création des données de test avant tous les tests ---
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
|
|
68
|
+
engine = await initEngine();
|
|
69
|
+
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// --- Tests pour la fonction principale de calcul ---
|
|
73
|
+
describe('getUserActivePermissions()', () => {
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
beforeEach(async () =>{
|
|
77
|
+
await setupTestContext();
|
|
78
|
+
})
|
|
79
|
+
it('should return base permissions from the user\'s role', async () => {
|
|
80
|
+
const permissions = await getUserActivePermissions(testUser);
|
|
81
|
+
expect(permissions).to.be.an.instanceOf(Set);
|
|
82
|
+
expect(permissions.size).to.equal(2);
|
|
83
|
+
expect(permissions.has('post:read')).to.be.true;
|
|
84
|
+
expect(permissions.has('post:write')).to.be.true;
|
|
85
|
+
expect(permissions.has('post:delete')).to.be.false;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should grant a temporary permission via an exception', async () => {
|
|
89
|
+
|
|
90
|
+
// Ajoute une exception qui donne la permission de supprimer, expirant demain
|
|
91
|
+
const futureDate = new Date();
|
|
92
|
+
futureDate.setDate(futureDate.getDate() + 1);
|
|
93
|
+
|
|
94
|
+
await testDatasColInstance.insertOne({
|
|
95
|
+
_model: 'userPermission',
|
|
96
|
+
user: testUser._id,
|
|
97
|
+
permission: permDelete,
|
|
98
|
+
isGranted: true,
|
|
99
|
+
expiresAt: futureDate
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const permissions = await getUserActivePermissions(testUser);
|
|
103
|
+
expect(permissions.size).to.equal(3);
|
|
104
|
+
expect(permissions.has('post:delete')).to.be.true;
|
|
105
|
+
|
|
106
|
+
// Nettoyage de l'exception pour ne pas affecter les autres tests
|
|
107
|
+
await testDatasColInstance.deleteOne({ _model: 'userPermission', user: testUser._id });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should NOT grant an expired temporary permission', async () => {
|
|
111
|
+
// Ajoute une exception qui a expiré hier
|
|
112
|
+
const pastDate = new Date();
|
|
113
|
+
pastDate.setDate(pastDate.getDate() - 1);
|
|
114
|
+
|
|
115
|
+
await testDatasColInstance.insertOne({
|
|
116
|
+
_model: 'userPermission',
|
|
117
|
+
user: testUser._id,
|
|
118
|
+
permission: permDelete,
|
|
119
|
+
isGranted: true,
|
|
120
|
+
expiresAt: pastDate
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const permissions = await getUserActivePermissions(testUser);
|
|
124
|
+
expect(permissions.size).to.equal(2); // Revient la normale
|
|
125
|
+
expect(permissions.has('post:delete')).to.be.false;
|
|
126
|
+
|
|
127
|
+
await testDatasColInstance.deleteOne({ _model: 'userPermission', user: testUser._id });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should revoke a base permission via an exception', async () => {
|
|
131
|
+
// L'utilisateur est "Editor", mais on lui retire le droit d'écriture temporairement
|
|
132
|
+
const futureDate = new Date();
|
|
133
|
+
futureDate.setDate(futureDate.getDate() + 1);
|
|
134
|
+
|
|
135
|
+
await testDatasColInstance.insertOne({
|
|
136
|
+
_model: 'userPermission',
|
|
137
|
+
user: testUser._id,
|
|
138
|
+
permission: permWrite,
|
|
139
|
+
isGranted: false, // Révocation
|
|
140
|
+
expiresAt: futureDate
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const permissions = await getUserActivePermissions(testUser);
|
|
144
|
+
expect(permissions.size).to.equal(1);
|
|
145
|
+
expect(permissions.has('post:read')).to.be.true;
|
|
146
|
+
expect(permissions.has('post:write')).to.be.false; // La permission a été retirée
|
|
147
|
+
|
|
148
|
+
await testDatasColInstance.deleteOne({ _model: 'userPermission', user: testUser._id });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should restore a revoked permission if the revocation has expired', async () => {
|
|
152
|
+
// La révocation a expiré, l'utilisateur devrait retrouver son droit d'écriture
|
|
153
|
+
const pastDate = new Date();
|
|
154
|
+
pastDate.setDate(pastDate.getDate() - 1);
|
|
155
|
+
|
|
156
|
+
await testDatasColInstance.insertOne({
|
|
157
|
+
_model: 'userPermission',
|
|
158
|
+
user: testUser._id,
|
|
159
|
+
permission: permWrite,
|
|
160
|
+
isGranted: false,
|
|
161
|
+
expiresAt: pastDate
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const permissions = await getUserActivePermissions(testUser);
|
|
165
|
+
expect(permissions.size).to.equal(2);
|
|
166
|
+
expect(permissions.has('post:write')).to.be.true; // La permission est de retour
|
|
167
|
+
|
|
168
|
+
await testDatasColInstance.deleteOne({ _model: 'userPermission', user: testUser._id });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// --- Tests pour la fonction publique `hasPermission` ---
|
|
173
|
+
describe('hasPermission()', () => {
|
|
174
|
+
beforeEach(async () => {
|
|
175
|
+
await setupTestContext();
|
|
176
|
+
});
|
|
177
|
+
it('should return true for a permission the user has', async () => {
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
console.log(await testDatasColInstance.find({ _model: 'permission' }).toArray());
|
|
181
|
+
console.log(await testDatasColInstance.find({ _model: 'role' }).toArray());
|
|
182
|
+
console.log(await testDatasColInstance.find({ _model: 'user' }).toArray());
|
|
183
|
+
console.log(await testDatasColInstance.find({ _model: 'userPermission' }).toArray());
|
|
184
|
+
const result = await hasPermission('post:read', testUser);
|
|
185
|
+
expect(result).to.be.true;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should return false for a permission the user does not have', async () => {
|
|
189
|
+
const result = await hasPermission('user:manage', testUser);
|
|
190
|
+
expect(result).to.be.false;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should return true if user has at least one of the required permissions', async () => {
|
|
194
|
+
const result = await hasPermission(['user:manage', 'post:write'], testUser);
|
|
195
|
+
expect(result).to.be.true;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return false if user has none of the required permissions', async () => {
|
|
199
|
+
const result = await hasPermission(['user:manage', 'post:delete'], testUser);
|
|
200
|
+
expect(result).to.be.false;
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|