data-primals-engine 1.2.1 → 1.2.3
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 +69 -9
- package/client/src/DataEditor.jsx +1 -202
- package/client/src/DataLayout.jsx +4 -21
- package/client/src/DataTable.jsx +4 -2
- package/client/src/RestoreConfirmationModal.jsx +0 -137
- package/client/src/constants.js +1 -1
- package/client/src/hooks/useTutorials.jsx +3 -3
- package/package.json +2 -2
- package/src/constants.js +2 -2
- package/src/data.js +4 -4
- package/src/defaultModels.js +1 -14
- package/src/email.js +8 -6
- package/src/engine.js +12 -8
- package/src/events.js +26 -4
- package/src/filter.js +221 -0
- package/src/modules/bucket.js +115 -5
- package/src/modules/data.js +174 -101
- package/src/modules/file.js +98 -57
- package/src/modules/test +147 -0
- package/src/modules/workflow.js +266 -100
- package/src/packs.js +245 -7
- package/test/data.backup.integration.test.js +5 -2
- package/test/data.integration.test.js +10 -4
- package/test/events.test.js +202 -0
- package/test/file.test.js +193 -0
- package/test/import_export.integration.test.js +23 -19
- package/test/model.integration.test.js +20 -19
- package/test/user.test.js +30 -23
- package/test/vm.test.js +51 -0
- package/test/workflow.integration.test.js +6 -1
- package/test/workflow.robustness.test.js +6 -1
|
@@ -0,0 +1,193 @@
|
|
|
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, getUserCollectionName} from "../src/modules/mongodb.js";
|
|
11
|
+
import { Logger } from "../src/gameObject.js";
|
|
12
|
+
import {maxPrivateFileSize} from "../src/constants.js";
|
|
13
|
+
import {getCollectionForUser} from "data-primals-engine/modules/mongodb";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
vi.stubEnv('ENCRYPTION_KEY', '12345678901234567890123456789012');
|
|
18
|
+
|
|
19
|
+
// Mock pour les fichiers uploadés
|
|
20
|
+
const createMockFile = (name, size, type = 'text/plain') => {
|
|
21
|
+
const tempPath = path.join(__dirname, 'temp', name);
|
|
22
|
+
fs.writeFileSync(tempPath, 'a'.repeat(size));
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
size,
|
|
26
|
+
type,
|
|
27
|
+
path: tempPath
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
let engine;
|
|
31
|
+
const testUploadDir = path.join(process.cwd(), "uploads", "private");
|
|
32
|
+
const testTempDir = path.join(__dirname, 'temp');
|
|
33
|
+
let filesCollection;
|
|
34
|
+
|
|
35
|
+
beforeAll(async () => {
|
|
36
|
+
Config.Set("modules", ["mongodb", "data", "file", "bucket", "user","file"]);
|
|
37
|
+
engine= await initEngine();
|
|
38
|
+
await onInit(engine);
|
|
39
|
+
|
|
40
|
+
// Créer les répertoires nécessaires
|
|
41
|
+
if (!fs.existsSync(testUploadDir)) {
|
|
42
|
+
fs.mkdirSync(testUploadDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
if (!fs.existsSync(testTempDir)) {
|
|
45
|
+
fs.mkdirSync(testTempDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
filesCollection = await getCollection("files");
|
|
49
|
+
});
|
|
50
|
+
let testUser = {
|
|
51
|
+
username: generateUniqueName('testuser'),
|
|
52
|
+
_user: generateUniqueName('testuser'),
|
|
53
|
+
userPlan: 'premium',
|
|
54
|
+
permissions: ['API_UPLOAD_FILE']
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe('File Module Integration Tests', () => {
|
|
58
|
+
|
|
59
|
+
beforeEach(async () => {
|
|
60
|
+
// Créer un utilisateur de test
|
|
61
|
+
// Nettoyer les collections avant chaque test
|
|
62
|
+
await filesCollection.deleteMany({});
|
|
63
|
+
|
|
64
|
+
// Nettoyer le répertoire upload
|
|
65
|
+
const files = fs.readdirSync(testUploadDir);
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
fs.unlinkSync(path.join(testUploadDir, file));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterAll(async () => {
|
|
72
|
+
// Nettoyer après tous les tests
|
|
73
|
+
await filesCollection.deleteMany({});
|
|
74
|
+
if (fs.existsSync(testUploadDir)) {
|
|
75
|
+
fs.rmSync(testUploadDir, { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
if (fs.existsSync(testTempDir)) {
|
|
78
|
+
fs.rmSync(testTempDir, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('addFile', () => {
|
|
83
|
+
it('should successfully add a file with local storage', async () => {
|
|
84
|
+
const mockFile = createMockFile('test.txt', 1024); // 1KB file
|
|
85
|
+
|
|
86
|
+
const guid = await addFile(mockFile, testUser);
|
|
87
|
+
|
|
88
|
+
// Vérifier le retour
|
|
89
|
+
expect(guid).toBeDefined();
|
|
90
|
+
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);
|
|
91
|
+
|
|
92
|
+
// Vérifier en base de données
|
|
93
|
+
const fileRecord = await filesCollection.findOne({ guid });
|
|
94
|
+
expect(fileRecord).toBeDefined();
|
|
95
|
+
expect(fileRecord.filename).toBeDefined();
|
|
96
|
+
expect(fileRecord.size).toBe(1024);
|
|
97
|
+
expect(fileRecord.user).toBe(testUser.username);
|
|
98
|
+
expect(fileRecord.storage).toBe('local');
|
|
99
|
+
|
|
100
|
+
// Vérifier que le fichier existe physiquement
|
|
101
|
+
const expectedPath = path.join(testUploadDir, `${guid}.txt`);
|
|
102
|
+
expect(fs.existsSync(expectedPath)).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should throw error when file size exceeds limit', async () => {
|
|
106
|
+
const mockFile = createMockFile('large.txt', maxPrivateFileSize+1); // 11MB (dépasse la limite de 10MB)
|
|
107
|
+
|
|
108
|
+
await expect(addFile(mockFile, testUser)).rejects.toThrow(/La taille du fichier dépasse la limite autorisée/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it.skip('should throw error when storage limit exceeded', async ({skip}) => {
|
|
112
|
+
// Mock la fonction de calcul d'usage pour simuler un dépassement
|
|
113
|
+
vi.spyOn(engine.userProvider, 'getUserStorageLimit').mockResolvedValue(85 * 1024 * 1024); // 5MB
|
|
114
|
+
|
|
115
|
+
const mockFile = createMockFile('test.txt', 1024 * 1024); // 1MB
|
|
116
|
+
|
|
117
|
+
await expect(addFile(mockFile, testUser)).rejects.toThrow("api.data.storageLimitExceeded");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it.skip('should throw error when server capacity is insufficient', async ({skip}) => {
|
|
121
|
+
// Mock la vérification de capacité serveur
|
|
122
|
+
vi.spyOn(engine.userProvider, 'checkServerCapacity').mockResolvedValue({ isSufficient: false });
|
|
123
|
+
|
|
124
|
+
const mockFile = createMockFile('test.txt', 1024);
|
|
125
|
+
|
|
126
|
+
await expect(addFile(mockFile, testUser)).rejects.toThrow("api.data.serverStorageFull");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('removeFile', () => {
|
|
131
|
+
it('should successfully remove a locally stored file', async () => {
|
|
132
|
+
// D'abord ajouter un fichier
|
|
133
|
+
const mockFile = createMockFile('to-delete.txt', 1024);
|
|
134
|
+
const guid = await addFile(mockFile, testUser);
|
|
135
|
+
|
|
136
|
+
// Vérifier qu'il existe avant suppression
|
|
137
|
+
const beforeDelete = await getFile(guid);
|
|
138
|
+
expect(beforeDelete).toBeDefined();
|
|
139
|
+
|
|
140
|
+
// Supprimer le fichier
|
|
141
|
+
await removeFile(guid, testUser);
|
|
142
|
+
|
|
143
|
+
// Vérifier en base de données
|
|
144
|
+
const afterDelete = await getFile(guid);
|
|
145
|
+
expect(afterDelete).toBeNull();
|
|
146
|
+
|
|
147
|
+
// Vérifier que le fichier physique a été supprimé
|
|
148
|
+
const expectedPath = path.join(testUploadDir, `${guid}.txt`);
|
|
149
|
+
expect(fs.existsSync(expectedPath)).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should handle trying to remove non-existent file gracefully', async () => {
|
|
153
|
+
const nonExistentGuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
154
|
+
|
|
155
|
+
vi.spyOn(engine.getComponent(Logger), "info");
|
|
156
|
+
|
|
157
|
+
// Cela ne devrait pas throw d'erreur
|
|
158
|
+
await removeFile(nonExistentGuid, testUser);
|
|
159
|
+
|
|
160
|
+
// Vérifier que le logger a bien enregistré l'info
|
|
161
|
+
expect(engine.getComponent(Logger).info).toHaveBeenCalledWith(
|
|
162
|
+
expect.stringContaining(`Tentative de suppression d'un fichier inexistant`),
|
|
163
|
+
'warn'
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should throw error when GUID is invalid', async () => {
|
|
168
|
+
await expect(removeFile('invalid-guid', testUser)).rejects.toThrow("Le GUID du fichier n'est pas valide.");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('getFile', () => {
|
|
173
|
+
it('should retrieve file metadata', async () => {
|
|
174
|
+
const mockFile = createMockFile('metadata-test.txt', 2048);
|
|
175
|
+
const guid = await addFile(mockFile, testUser);
|
|
176
|
+
|
|
177
|
+
const fileData = await getFile(guid);
|
|
178
|
+
|
|
179
|
+
expect(fileData).toBeDefined();
|
|
180
|
+
expect(fileData.guid).toBe(guid);
|
|
181
|
+
expect(fileData.filename).toContain('.txt');
|
|
182
|
+
expect(fileData.size).toBe(2048);
|
|
183
|
+
expect(fileData.user).toBe(testUser.username);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return null for non-existent file', async () => {
|
|
187
|
+
const nonExistentGuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
188
|
+
const fileData = await getFile(nonExistentGuid);
|
|
189
|
+
|
|
190
|
+
expect(fileData).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -2,20 +2,20 @@ import {expect, describe, it, beforeEach, beforeAll, afterAll, vi} from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
// --- Importations des modules de votre application ---
|
|
4
4
|
import {
|
|
5
|
-
createModel,
|
|
6
5
|
insertData,
|
|
7
6
|
exportData,
|
|
8
7
|
importData
|
|
9
|
-
} from '
|
|
8
|
+
} from '../src/index.js';
|
|
10
9
|
|
|
11
10
|
import {
|
|
12
11
|
modelsCollection as getAppModelsCollection,
|
|
13
|
-
getCollectionForUser as getAppUserCollection
|
|
12
|
+
getCollectionForUser as getAppUserCollection, getCollectionForUser
|
|
14
13
|
} from 'data-primals-engine/modules/mongodb';
|
|
15
14
|
import {sleep} from "data-primals-engine/core";
|
|
16
15
|
import fs from "node:fs";
|
|
17
16
|
import {getUniquePort, initEngine} from "../src/setenv.js";
|
|
18
17
|
import {Config} from "../src/index.js";
|
|
18
|
+
import {ObjectId} from "mongodb";
|
|
19
19
|
|
|
20
20
|
// --- Données Mock ---
|
|
21
21
|
const mockUser = {
|
|
@@ -56,6 +56,10 @@ beforeAll(async () =>{
|
|
|
56
56
|
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
|
|
57
57
|
await initEngine();
|
|
58
58
|
})
|
|
59
|
+
afterAll(async () => {
|
|
60
|
+
const coll = await getCollectionForUser(mockUser);
|
|
61
|
+
await coll.drop();
|
|
62
|
+
})
|
|
59
63
|
// --- Début des tests ---
|
|
60
64
|
describe('Intégration des fonctions d\'Import/Export', () => {
|
|
61
65
|
|
|
@@ -114,13 +118,13 @@ describe('Intégration des fonctions d\'Import/Export', () => {
|
|
|
114
118
|
fs.writeFileSync('test.json', jsonString);
|
|
115
119
|
blob.path = 'test.json';
|
|
116
120
|
blob.originalFilename = 'test.json';
|
|
117
|
-
const result = await importData({model: impexTestModel.name}, {file: blobToFile(blob,"test.json")}, mockUser);
|
|
121
|
+
const result = await importData({model: impexTestModel.name}, {file: blobToFile(blob, "test.json")}, mockUser);
|
|
118
122
|
|
|
119
123
|
// Vérifications du résultat de l'opération
|
|
120
124
|
expect(result.success).toBe(true);
|
|
121
|
-
expect(result.jobId).not.toBeNull();
|
|
125
|
+
expect(result.job.jobId).not.toBeNull();
|
|
122
126
|
|
|
123
|
-
await sleep(
|
|
127
|
+
await sleep(8000)
|
|
124
128
|
|
|
125
129
|
// Vérification directe en base de données
|
|
126
130
|
const importedDocs = await testDatasColInstance.find({
|
|
@@ -136,7 +140,7 @@ describe('Intégration des fonctions d\'Import/Export', () => {
|
|
|
136
140
|
expect(docD.inStock).toBe(true); // Vérification de la valeur par défaut
|
|
137
141
|
expect(docE.price).toBe(2.00);
|
|
138
142
|
|
|
139
|
-
},
|
|
143
|
+
}, 20000);
|
|
140
144
|
|
|
141
145
|
it('devrait importer des données depuis une chaîne CSV et convertir les types', async () => {
|
|
142
146
|
const csvStringToImport = `name,sku,price,inStock\nProduit F,SKU-F,3.55,true\nProduit G,SKU-G,4.99,false`;
|
|
@@ -148,14 +152,13 @@ describe('Intégration des fonctions d\'Import/Export', () => {
|
|
|
148
152
|
blob.originalFilename = 'test.csv';
|
|
149
153
|
|
|
150
154
|
// Exécution de la fonction d'import
|
|
151
|
-
const result = await importData({model:impexTestModel.name}, {file: blobToFile(blob,"test.csv")}, mockUser);
|
|
155
|
+
const result = await importData({model:impexTestModel.name}, {file: blobToFile(blob, "test.csv")}, mockUser);
|
|
152
156
|
|
|
153
|
-
console.log(result)
|
|
154
157
|
// Vérifications du résultat
|
|
155
158
|
expect(result.success).toBe(true);
|
|
159
|
+
expect(result.job.jobId).not.toBeNull();
|
|
156
160
|
|
|
157
|
-
await sleep(
|
|
158
|
-
|
|
161
|
+
await sleep(8000);
|
|
159
162
|
// Vérification en base de données
|
|
160
163
|
const importedDocs = await testDatasColInstance.find({
|
|
161
164
|
_model: impexTestModel.name,
|
|
@@ -185,16 +188,17 @@ Valide K,SKU-A,40,true`;
|
|
|
185
188
|
blob.path = 'test.csv';
|
|
186
189
|
blob.originalFilename = 'test.csv';
|
|
187
190
|
|
|
188
|
-
const result = await importData(impexTestModel.name, {file: blobToFile(blob,"test.csv")},
|
|
191
|
+
const result = await importData({ model: impexTestModel.name }, {file: blobToFile(blob,"test.csv")}, mockUser);
|
|
189
192
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
expect(result.
|
|
193
|
-
|
|
193
|
+
// L'initiation du job doit réussir
|
|
194
|
+
expect(result.success).toBe(true);
|
|
195
|
+
expect(result.job.jobId).not.toBeNull();
|
|
196
|
+
|
|
197
|
+
await sleep(8000);
|
|
194
198
|
|
|
195
199
|
// Vérifier que seule les données valides sont en BDD
|
|
196
|
-
const count = await testDatasColInstance.countDocuments({ _model: impexTestModel.name });
|
|
197
|
-
expect(count).toBe(
|
|
198
|
-
});
|
|
200
|
+
const count = await testDatasColInstance.countDocuments({ _model: impexTestModel.name, sku: 'SKU-H' });
|
|
201
|
+
expect(count).toBe(1); // Seule la ligne valide 'SKU-H' doit être insérée.
|
|
202
|
+
}, 20000);
|
|
199
203
|
});
|
|
200
204
|
});
|
|
@@ -5,30 +5,31 @@ import { Config } from '../src/config.js';
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
modelsCollection as getAppModelsCollection,
|
|
8
|
-
datasCollection // Accès direct pour vérifications
|
|
8
|
+
datasCollection, getCollection // Accès direct pour vérifications
|
|
9
9
|
} from 'data-primals-engine/modules/mongodb';
|
|
10
10
|
import {generateUniqueName, getUniquePort, initEngine} from "../src/setenv.js";
|
|
11
11
|
import {editModel} from "../src/modules/data.js";
|
|
12
|
-
import {getCollectionForUser} from "../src/modules/mongodb.js";
|
|
12
|
+
import {getCollectionForUser, getUserCollectionName} from "../src/modules/mongodb.js";
|
|
13
13
|
|
|
14
14
|
let testModelsColInstance;
|
|
15
|
-
let testDatasColInstance;
|
|
16
|
-
|
|
17
15
|
let testModelId;
|
|
18
|
-
|
|
16
|
+
let lastUser;
|
|
19
17
|
// Cette fonction va remplacer la logique de votre beforeEach pour la création de contexte
|
|
20
18
|
async function setupTestContext() {
|
|
21
19
|
|
|
22
20
|
const currentTestModelName = generateUniqueName('relatedModel');
|
|
23
21
|
const currentRelatedModelName = generateUniqueName('comprehensiveModel');
|
|
24
22
|
|
|
23
|
+
|
|
25
24
|
// Créer un utilisateur unique pour ce test
|
|
26
25
|
const currentTestUser = {
|
|
27
|
-
username: generateUniqueName('
|
|
28
|
-
userPlan: '
|
|
26
|
+
username: generateUniqueName('testuserModelIntegration'),
|
|
27
|
+
userPlan: 'premium',
|
|
29
28
|
email: generateUniqueName('test') + '@example.com'
|
|
30
29
|
};
|
|
31
30
|
|
|
31
|
+
testModelsColInstance = getCollection("models");
|
|
32
|
+
const testDatasColInstance = await getCollectionForUser(currentTestUser);
|
|
32
33
|
|
|
33
34
|
const relatedModelDefinition = {
|
|
34
35
|
name: currentRelatedModelName,
|
|
@@ -105,43 +106,42 @@ async function setupTestContext() {
|
|
|
105
106
|
await testDatasColInstance.deleteMany({ _user: currentTestUser.username });
|
|
106
107
|
await testDatasColInstance.deleteMany({ _model: { $in: [comprehensiveTestModelDefinition.name, 'renamedTestModel'] } });
|
|
107
108
|
// Retourner toutes les variables nécessaires pour un test
|
|
109
|
+
|
|
108
110
|
return {
|
|
109
111
|
currentTestUser,
|
|
112
|
+
coll:testDatasColInstance,
|
|
110
113
|
comprehensiveTestModelDefinition,
|
|
111
114
|
relatedModelDefinition
|
|
112
115
|
};
|
|
113
116
|
}
|
|
114
117
|
|
|
118
|
+
|
|
115
119
|
describe('CRUD on model definitions and integrity tests', () => {
|
|
116
120
|
|
|
117
121
|
beforeAll(async () =>{
|
|
118
122
|
Config.Set("modules", ["mongodb", "data", "file", "bucket", "workflow","user", "assistant"]);
|
|
119
123
|
await initEngine();
|
|
120
|
-
|
|
121
|
-
// Initialize collection instances after the engine is ready
|
|
122
|
-
testModelsColInstance = getAppModelsCollection;
|
|
123
|
-
testDatasColInstance = datasCollection;
|
|
124
124
|
})
|
|
125
|
+
|
|
125
126
|
describe('editModel unit tests', () => {
|
|
126
127
|
|
|
127
|
-
it('should create and drop index when field.index is toggled (premium user)', async () => {
|
|
128
|
+
it.skip('should create and drop index when field.index is toggled (premium user)', async () => {
|
|
128
129
|
// --- SETUP ---
|
|
129
|
-
const { currentTestUser, comprehensiveTestModelDefinition } = await setupTestContext();
|
|
130
|
-
const dataCollection = await getCollectionForUser(currentTestUser);
|
|
130
|
+
const { coll, currentTestUser, comprehensiveTestModelDefinition } = await setupTestContext();
|
|
131
131
|
const fieldToIndex = 'stringUnique'; // Utiliser un champ qui existe vraiment dans le modèle
|
|
132
132
|
|
|
133
133
|
// --- FIX: Ensure the collection exists before any operation ---
|
|
134
134
|
// By inserting and deleting a dummy document, we force MongoDB to create the
|
|
135
135
|
// collection and its default indexes. This prevents the "ns does not exist"
|
|
136
136
|
// error in asynchronous listeners (like workflow triggers).
|
|
137
|
-
const dummyDoc = await
|
|
138
|
-
await
|
|
137
|
+
const dummyDoc = await coll.insertOne({ _model: 'dummy', _user: currentTestUser.username });
|
|
138
|
+
await coll.deleteOne({ _id: dummyDoc.insertedId });
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
// --- VERIFICATION INITIALE ---
|
|
142
142
|
// S'assurer qu'aucun index n'existe au départ.
|
|
143
143
|
// Cet appel ne plantera plus car la collection est maintenant créée.
|
|
144
|
-
const initialIndexes = await
|
|
144
|
+
const initialIndexes = await coll.indexes();
|
|
145
145
|
expect(initialIndexes.some(i => i.key[fieldToIndex] === 1)).toBe(false);
|
|
146
146
|
|
|
147
147
|
// --- ACTION 1 : AJOUTER UN INDEX ---
|
|
@@ -155,7 +155,7 @@ describe('CRUD on model definitions and integrity tests', () => {
|
|
|
155
155
|
|
|
156
156
|
// --- VERIFICATION 1 ---
|
|
157
157
|
// Maintenant, la collection et l'index doivent exister.
|
|
158
|
-
const indexesAfterCreation = await
|
|
158
|
+
const indexesAfterCreation = await coll.indexes();
|
|
159
159
|
const newIndex = indexesAfterCreation.find(i => i.key[fieldToIndex] === 1);
|
|
160
160
|
|
|
161
161
|
expect(newIndex).toBeDefined();
|
|
@@ -175,8 +175,9 @@ describe('CRUD on model definitions and integrity tests', () => {
|
|
|
175
175
|
await editModel(currentTestUser, testModelId, modelWithoutIndex);
|
|
176
176
|
|
|
177
177
|
// --- VERIFICATION 2 ---
|
|
178
|
-
const indexesAfterDeletion = await
|
|
178
|
+
const indexesAfterDeletion = await coll.indexes();
|
|
179
179
|
expect(indexesAfterDeletion.some(i => i.key[fieldToIndex] === 1)).toBe(false);
|
|
180
|
+
|
|
180
181
|
}, 20000);
|
|
181
182
|
|
|
182
183
|
it('should not save extra, non-defined fields in the model definition', async () => {
|
package/test/user.test.js
CHANGED
|
@@ -17,7 +17,7 @@ async function setupTestContext() {
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
// Créer un utilisateur unique pour ce test
|
|
20
|
-
const username = generateUniqueName('
|
|
20
|
+
const username = generateUniqueName('testuserUserIntegration');
|
|
21
21
|
currentTestUser = {
|
|
22
22
|
username,
|
|
23
23
|
userPlan: 'free',
|
|
@@ -58,6 +58,8 @@ async function setupTestContext() {
|
|
|
58
58
|
]);
|
|
59
59
|
testUser = {...currentTestUser, _id: users.insertedIds[0].toString(), roles: [roleEditor.toString()] };
|
|
60
60
|
adminUser = {...currentTestUser, _id: users.insertedIds[1].toString(), roles: [roleEditor.toString()] };
|
|
61
|
+
|
|
62
|
+
return testDatasColInstance;
|
|
61
63
|
};
|
|
62
64
|
let engine;
|
|
63
65
|
describe('User Permission Logic', () => {
|
|
@@ -72,26 +74,24 @@ describe('User Permission Logic', () => {
|
|
|
72
74
|
// --- Tests pour la fonction principale de calcul ---
|
|
73
75
|
describe('getUserActivePermissions()', () => {
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
beforeEach(async () =>{
|
|
77
|
-
await setupTestContext();
|
|
78
|
-
})
|
|
79
77
|
it('should return base permissions from the user\'s role', async () => {
|
|
78
|
+
const coll = await setupTestContext();
|
|
80
79
|
const permissions = await getUserActivePermissions(testUser);
|
|
81
80
|
expect(permissions).to.be.an.instanceOf(Set);
|
|
82
81
|
expect(permissions.size).to.equal(2);
|
|
83
82
|
expect(permissions.has('post:read')).to.be.true;
|
|
84
83
|
expect(permissions.has('post:write')).to.be.true;
|
|
85
84
|
expect(permissions.has('post:delete')).to.be.false;
|
|
85
|
+
await coll.drop();
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('should grant a temporary permission via an exception', async () => {
|
|
89
|
-
|
|
89
|
+
const coll = await setupTestContext();
|
|
90
90
|
// Ajoute une exception qui donne la permission de supprimer, expirant demain
|
|
91
91
|
const futureDate = new Date();
|
|
92
92
|
futureDate.setDate(futureDate.getDate() + 1);
|
|
93
93
|
|
|
94
|
-
await
|
|
94
|
+
await coll.insertOne({
|
|
95
95
|
_model: 'userPermission',
|
|
96
96
|
user: testUser._id,
|
|
97
97
|
permission: permDelete,
|
|
@@ -104,15 +104,18 @@ describe('User Permission Logic', () => {
|
|
|
104
104
|
expect(permissions.has('post:delete')).to.be.true;
|
|
105
105
|
|
|
106
106
|
// Nettoyage de l'exception pour ne pas affecter les autres tests
|
|
107
|
-
await
|
|
107
|
+
await coll.deleteOne({ _model: 'userPermission', user: testUser._id });
|
|
108
|
+
|
|
109
|
+
await coll.drop();
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
it('should NOT grant an expired temporary permission', async () => {
|
|
113
|
+
const coll = await setupTestContext();
|
|
111
114
|
// Ajoute une exception qui a expiré hier
|
|
112
115
|
const pastDate = new Date();
|
|
113
116
|
pastDate.setDate(pastDate.getDate() - 1);
|
|
114
117
|
|
|
115
|
-
await
|
|
118
|
+
await coll.insertOne({
|
|
116
119
|
_model: 'userPermission',
|
|
117
120
|
user: testUser._id,
|
|
118
121
|
permission: permDelete,
|
|
@@ -124,15 +127,17 @@ describe('User Permission Logic', () => {
|
|
|
124
127
|
expect(permissions.size).to.equal(2); // Revient la normale
|
|
125
128
|
expect(permissions.has('post:delete')).to.be.false;
|
|
126
129
|
|
|
127
|
-
await
|
|
130
|
+
await coll.deleteOne({ _model: 'userPermission', user: testUser._id });
|
|
131
|
+
await coll.drop();
|
|
128
132
|
});
|
|
129
133
|
|
|
130
134
|
it('should revoke a base permission via an exception', async () => {
|
|
135
|
+
const coll = await setupTestContext();
|
|
131
136
|
// L'utilisateur est "Editor", mais on lui retire le droit d'écriture temporairement
|
|
132
137
|
const futureDate = new Date();
|
|
133
138
|
futureDate.setDate(futureDate.getDate() + 1);
|
|
134
139
|
|
|
135
|
-
await
|
|
140
|
+
await coll.insertOne({
|
|
136
141
|
_model: 'userPermission',
|
|
137
142
|
user: testUser._id,
|
|
138
143
|
permission: permWrite,
|
|
@@ -145,15 +150,17 @@ describe('User Permission Logic', () => {
|
|
|
145
150
|
expect(permissions.has('post:read')).to.be.true;
|
|
146
151
|
expect(permissions.has('post:write')).to.be.false; // La permission a été retirée
|
|
147
152
|
|
|
148
|
-
await
|
|
153
|
+
await coll.deleteOne({ _model: 'userPermission', user: testUser._id });
|
|
154
|
+
await coll.drop();
|
|
149
155
|
});
|
|
150
156
|
|
|
151
157
|
it('should restore a revoked permission if the revocation has expired', async () => {
|
|
158
|
+
const coll = await setupTestContext();
|
|
152
159
|
// La révocation a expiré, l'utilisateur devrait retrouver son droit d'écriture
|
|
153
160
|
const pastDate = new Date();
|
|
154
161
|
pastDate.setDate(pastDate.getDate() - 1);
|
|
155
162
|
|
|
156
|
-
await
|
|
163
|
+
await coll.insertOne({
|
|
157
164
|
_model: 'userPermission',
|
|
158
165
|
user: testUser._id,
|
|
159
166
|
permission: permWrite,
|
|
@@ -165,39 +172,39 @@ describe('User Permission Logic', () => {
|
|
|
165
172
|
expect(permissions.size).to.equal(2);
|
|
166
173
|
expect(permissions.has('post:write')).to.be.true; // La permission est de retour
|
|
167
174
|
|
|
168
|
-
await
|
|
175
|
+
await coll.deleteOne({ _model: 'userPermission', user: testUser._id });
|
|
176
|
+
await coll.drop();
|
|
169
177
|
});
|
|
170
178
|
});
|
|
171
179
|
|
|
172
180
|
// --- Tests pour la fonction publique `hasPermission` ---
|
|
173
181
|
describe('hasPermission()', () => {
|
|
174
|
-
beforeEach(async () => {
|
|
175
|
-
await setupTestContext();
|
|
176
|
-
});
|
|
177
182
|
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());
|
|
183
|
+
const coll = await setupTestContext();
|
|
184
184
|
const result = await hasPermission('post:read', testUser);
|
|
185
185
|
expect(result).to.be.true;
|
|
186
|
+
await coll.drop();
|
|
186
187
|
});
|
|
187
188
|
|
|
188
189
|
it('should return false for a permission the user does not have', async () => {
|
|
190
|
+
const coll = await setupTestContext();
|
|
189
191
|
const result = await hasPermission('user:manage', testUser);
|
|
190
192
|
expect(result).to.be.false;
|
|
193
|
+
await coll.drop();
|
|
191
194
|
});
|
|
192
195
|
|
|
193
196
|
it('should return true if user has at least one of the required permissions', async () => {
|
|
197
|
+
const coll = await setupTestContext();
|
|
194
198
|
const result = await hasPermission(['user:manage', 'post:write'], testUser);
|
|
195
199
|
expect(result).to.be.true;
|
|
200
|
+
await coll.drop();
|
|
196
201
|
});
|
|
197
202
|
|
|
198
203
|
it('should return false if user has none of the required permissions', async () => {
|
|
204
|
+
const coll = await setupTestContext();
|
|
199
205
|
const result = await hasPermission(['user:manage', 'post:delete'], testUser);
|
|
200
206
|
expect(result).to.be.false;
|
|
207
|
+
await coll.drop();
|
|
201
208
|
});
|
|
202
209
|
});
|
|
203
210
|
});
|
package/test/vm.test.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {expect, describe, it, beforeEach, afterEach, beforeAll, afterAll, vi, vitest} from 'vitest';
|
|
2
|
+
import ivm from "isolated-vm";
|
|
3
|
+
import {sleep} from "data-primals-engine/core";
|
|
4
|
+
|
|
5
|
+
vi.stubEnv('ENCRYPTION_KEY', '12345678901234567890123456789012');
|
|
6
|
+
|
|
7
|
+
async function executeSafeJavascript() {
|
|
8
|
+
const isolate = new ivm.Isolate()
|
|
9
|
+
|
|
10
|
+
async function myAsyncFunction(str) {
|
|
11
|
+
await sleep(1000);
|
|
12
|
+
console.log("hello"+str)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const context = await isolate.createContext();
|
|
16
|
+
const jail = context.global;
|
|
17
|
+
|
|
18
|
+
await jail.set('myAsync', new ivm.Reference(myAsyncFunction));
|
|
19
|
+
|
|
20
|
+
const fn = await context.eval(`
|
|
21
|
+
const normalizeArgs = args => args.map(arg => {
|
|
22
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
23
|
+
return JSON.stringify(arg); // Convert objects to strings
|
|
24
|
+
}
|
|
25
|
+
return arg;
|
|
26
|
+
});
|
|
27
|
+
const t = (...args) => {
|
|
28
|
+
myAsync.applySyncPromise(null, normalizeArgs(args));
|
|
29
|
+
}
|
|
30
|
+
(async function untrusted() {
|
|
31
|
+
let str = await t("ok", {"ok":true});
|
|
32
|
+
return str;
|
|
33
|
+
})
|
|
34
|
+
`, { reference: true })
|
|
35
|
+
const value = await fn.apply(undefined, [], { result: { promise: true } })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
});
|
|
41
|
+
describe('VM system ingration', () => {
|
|
42
|
+
|
|
43
|
+
it('should successfully add a file with local storage', async () => {
|
|
44
|
+
console.log(await executeSafeJavascript({
|
|
45
|
+
script: 'const t = await addSync();'
|
|
46
|
+
}, {}, { username:'test'}));
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
expect(true).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -7,6 +7,7 @@ import { modelsCollection as getAppModelsCollection, getCollectionForUser } from
|
|
|
7
7
|
import * as workflowModule from 'data-primals-engine/modules/workflow';
|
|
8
8
|
import {getUniquePort, initEngine} from "../src/setenv.js";
|
|
9
9
|
import process from "process";
|
|
10
|
+
import {getUserCollectionName} from "../src/modules/mongodb.js";
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
beforeAll(async () =>{
|
|
@@ -18,7 +19,7 @@ vi.mock('data-primals-engine/modules/workflow', { spy: true })
|
|
|
18
19
|
const mockUser = {
|
|
19
20
|
username: 'testuserWorkflow',
|
|
20
21
|
_user: 'testuserWorkflow',
|
|
21
|
-
userPlan: '
|
|
22
|
+
userPlan: 'premium',
|
|
22
23
|
email: 'testWorkflow@example.com'
|
|
23
24
|
};
|
|
24
25
|
|
|
@@ -155,6 +156,10 @@ beforeEach(async () => {
|
|
|
155
156
|
console.log({mods})
|
|
156
157
|
});
|
|
157
158
|
|
|
159
|
+
afterAll(async () => {
|
|
160
|
+
const coll = await getCollectionForUser(mockUser);
|
|
161
|
+
await coll.drop();
|
|
162
|
+
})
|
|
158
163
|
describe('Intégration des Workflows - triggerWorkflows', () => {
|
|
159
164
|
|
|
160
165
|
let testWorkflow;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
|
-
import {
|
|
2
|
+
import {expect, describe, it, beforeEach, beforeAll, vi, afterAll} from 'vitest';
|
|
3
3
|
import { Config } from "data-primals-engine/config";
|
|
4
4
|
// --- Importations des modules de l'application ---
|
|
5
5
|
import { insertData, editData } from 'data-primals-engine/modules/data';
|
|
@@ -7,6 +7,7 @@ import { modelsCollection as getAppModelsCollection, getCollectionForUser, getCo
|
|
|
7
7
|
import * as workflowModule from 'data-primals-engine/modules/workflow';
|
|
8
8
|
import {initEngine} from "../src/setenv.js";
|
|
9
9
|
import {maxExecutionsByStep} from "../src/constants.js";
|
|
10
|
+
import {getUserCollectionName} from "../src/modules/mongodb.js";
|
|
10
11
|
|
|
11
12
|
vi.mock('data-primals-engine/modules/workflow', { spy: true })
|
|
12
13
|
|
|
@@ -47,6 +48,10 @@ beforeEach(async () => {
|
|
|
47
48
|
}
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
afterAll(async () => {
|
|
52
|
+
const coll = await getCollectionForUser(mockUser);
|
|
53
|
+
await coll.drop();
|
|
54
|
+
})
|
|
50
55
|
// ====================================================================================
|
|
51
56
|
// =================== DÉBUT DES TESTS DE ROBUSTESSE ==================================
|
|
52
57
|
// ====================================================================================
|