data-primals-engine 1.6.2-rc1 → 1.6.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 +61 -20
- package/client/src/Dashboard.jsx +1 -1
- package/client/src/WorkflowEditor.jsx +101 -0
- package/client/src/WorkflowEditor.scss +29 -4
- package/package.json +145 -142
- package/src/client.js +4 -0
- package/src/defaultModels +1628 -0
- package/src/defaultModels.js +14 -0
- package/src/filter.js +170 -1
- package/src/modules/assistant/assistant.js +38 -56
- package/src/modules/assistant/providers.js +38 -0
- package/src/modules/data/data.history.js +7 -6
- package/src/modules/data/data.operations.js +20 -5
- package/src/modules/user.js +57 -25
- package/src/modules/workflow.js +53 -178
- package/src/providers.js +1 -1
- package/test/assistant.test.js +207 -0
- package/test/config.test.js +41 -0
- package/test/data.history.integration.test.js +144 -20
- package/test/user.test.js +195 -278
- package/test/workflow.integration.test.js +8 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// __tests__/assistant.test.js
|
|
2
|
+
import { vi, expect, describe, it, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
3
|
+
import { Config } from '../src/config.js';
|
|
4
|
+
import { getCollection, getCollectionForUser } from '../src/modules/mongodb.js';
|
|
5
|
+
import { generateUniqueName, initEngine } from "../src/setenv.js";
|
|
6
|
+
import { purgeData } from "../src/modules/data/data.history.js";
|
|
7
|
+
import { handleChatRequest } from '../src/modules/assistant/assistant.js';
|
|
8
|
+
import { insertData } from '../src/index.js';
|
|
9
|
+
import * as dataOperations from '../src/modules/data/data.operations.js';
|
|
10
|
+
|
|
11
|
+
// --- CORRECTION ---
|
|
12
|
+
// On mock le nouveau module 'providers.js' qui contient la fonction que l'on veut surcharger.
|
|
13
|
+
vi.mock('../src/modules/assistant/providers.js', () => ({
|
|
14
|
+
getAIProvider: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// On importe la version mockée de getAIProvider
|
|
18
|
+
import { getAIProvider } from '../src/modules/assistant/providers.js';
|
|
19
|
+
|
|
20
|
+
let testUser;
|
|
21
|
+
let modelsCollection;
|
|
22
|
+
let datasCollection;
|
|
23
|
+
|
|
24
|
+
const modelName = generateUniqueName('productAssistant');
|
|
25
|
+
|
|
26
|
+
describe('Assistant Module Unit Tests', () => {
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
Config.Set("modules", ["mongodb", "data", "user", "assistant"]);
|
|
30
|
+
await initEngine(); // Cette ligne ne devrait plus planter.
|
|
31
|
+
testUser = {
|
|
32
|
+
username: generateUniqueName('testUserAssistant'),
|
|
33
|
+
email: generateUniqueName('test') + '@example.com',
|
|
34
|
+
lang: 'fr'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
modelsCollection = getCollection('models');
|
|
38
|
+
datasCollection = await getCollectionForUser(testUser);
|
|
39
|
+
|
|
40
|
+
// Create a model for testing
|
|
41
|
+
const productModelDef = {
|
|
42
|
+
name: modelName,
|
|
43
|
+
description: 'A model for testing',
|
|
44
|
+
_user: testUser.username,
|
|
45
|
+
fields: [
|
|
46
|
+
{ name: 'name', type: 'string', required: true },
|
|
47
|
+
{ name: 'price', type: 'number' }
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
await modelsCollection.insertOne(productModelDef);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
beforeEach(async () => {
|
|
54
|
+
// Clear mocks before each test
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
// Clean data, but keep the model
|
|
57
|
+
await datasCollection.deleteMany({ _user: testUser.username });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Helper to create a mock LLM stream
|
|
61
|
+
const createMockStream = (content) => {
|
|
62
|
+
return (async function* () {
|
|
63
|
+
yield { content };
|
|
64
|
+
})();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it('should return a simple text message from the AI', async () => {
|
|
68
|
+
const mockLLM = {
|
|
69
|
+
stream: vi.fn().mockReturnValue(createMockStream('Bonjour, ceci est un test.')),
|
|
70
|
+
};
|
|
71
|
+
getAIProvider.mockResolvedValue(mockLLM);
|
|
72
|
+
|
|
73
|
+
const params = { message: 'Dis bonjour', history: [] };
|
|
74
|
+
const result = await handleChatRequest(params, testUser);
|
|
75
|
+
|
|
76
|
+
expect(getAIProvider).toHaveBeenCalled();
|
|
77
|
+
expect(mockLLM.stream).toHaveBeenCalled();
|
|
78
|
+
expect(result).toEqual({
|
|
79
|
+
success: true,
|
|
80
|
+
displayMessage: 'Bonjour, ceci est un test.'
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle a search action', async () => {
|
|
85
|
+
|
|
86
|
+
// Configure le retour du mock pour ce test spécifique
|
|
87
|
+
const searchDataSpy = vi.spyOn(dataOperations, 'searchData').mockResolvedValue({ data: [{ name: 'Test Widget', price: 100 }], count: 1 });
|
|
88
|
+
|
|
89
|
+
const aiResponse = JSON.stringify([
|
|
90
|
+
{ action: 'search_models', params: { query: modelName } },
|
|
91
|
+
{ action: 'search', params: { model: modelName, filter: {} } }
|
|
92
|
+
]);
|
|
93
|
+
const mockLLM = {
|
|
94
|
+
stream: vi.fn().mockReturnValue(createMockStream(aiResponse))
|
|
95
|
+
};
|
|
96
|
+
getAIProvider.mockResolvedValue(mockLLM);
|
|
97
|
+
|
|
98
|
+
const params = { message: `cherche les produits`, history: [] };
|
|
99
|
+
const result = await handleChatRequest(params, testUser);
|
|
100
|
+
|
|
101
|
+
expect(searchDataSpy).toHaveBeenCalled();
|
|
102
|
+
|
|
103
|
+
expect(result.success).toBe(true);
|
|
104
|
+
expect(result.dataResult).toBeDefined();
|
|
105
|
+
expect(result.dataResult.model).toBe(modelName);
|
|
106
|
+
expect(result.dataResult.data).toHaveLength(1);
|
|
107
|
+
expect(result.dataResult.data[0].name).toBe('Test Widget');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should request confirmation for a "post" action', async () => {
|
|
111
|
+
const aiResponse = JSON.stringify({
|
|
112
|
+
action: 'post',
|
|
113
|
+
params: {
|
|
114
|
+
model: modelName,
|
|
115
|
+
data: { name: 'New Gadget', price: 19.99 }
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
const mockLLM = {
|
|
119
|
+
stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
|
|
120
|
+
};
|
|
121
|
+
getAIProvider.mockResolvedValue(mockLLM);
|
|
122
|
+
|
|
123
|
+
const params = { message: 'Crée un produit', history: [] };
|
|
124
|
+
const result = await handleChatRequest(params, testUser);
|
|
125
|
+
|
|
126
|
+
expect(result.success).toBe(true);
|
|
127
|
+
expect(result.confirmationRequest).toBeDefined();
|
|
128
|
+
expect(result.confirmationRequest.action).toBe('post');
|
|
129
|
+
expect(result.confirmationRequest.params.model).toBe(modelName);
|
|
130
|
+
expect(result.confirmationRequest.params.data.name).toBe('New Gadget');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should execute a confirmed "post" action', async () => {
|
|
134
|
+
const confirmedAction = {
|
|
135
|
+
action: 'post',
|
|
136
|
+
params: {
|
|
137
|
+
model: modelName,
|
|
138
|
+
data: { name: 'Confirmed Gadget', price: 50 }
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const params = { message: '', confirmedAction };
|
|
143
|
+
const result = await handleChatRequest(params, testUser);
|
|
144
|
+
|
|
145
|
+
expect(result.success).toBe(true);
|
|
146
|
+
// --- CORRECTION ---
|
|
147
|
+
// On vérifie le message de succès spécifique à la création, qui est plus informatif.
|
|
148
|
+
expect(result.displayMessage).toContain("Élément créé avec l'ID:");
|
|
149
|
+
|
|
150
|
+
// Verify the data was actually inserted
|
|
151
|
+
const dataInDb = await datasCollection.findOne({ name: 'Confirmed Gadget' });
|
|
152
|
+
expect(dataInDb).not.toBeNull();
|
|
153
|
+
expect(dataInDb.price).toBe(50);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle malformed JSON from AI by returning it as a text message', async () => {
|
|
157
|
+
const aiResponse = `{ "action": "search", "params": { "model": "${modelName}" }`; // Missing closing brace
|
|
158
|
+
const mockLLM = {
|
|
159
|
+
stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
|
|
160
|
+
};
|
|
161
|
+
getAIProvider.mockResolvedValue(mockLLM);
|
|
162
|
+
|
|
163
|
+
const params = { message: 'test', history: [] };
|
|
164
|
+
const result = await handleChatRequest(params, testUser);
|
|
165
|
+
|
|
166
|
+
expect(result.success).toBe(true);
|
|
167
|
+
expect(result.displayMessage).toBe(aiResponse);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should correct a malformed filter from the AI', async () => {
|
|
171
|
+
const aiResponse = JSON.stringify({
|
|
172
|
+
action: 'search',
|
|
173
|
+
params: {
|
|
174
|
+
model: modelName,
|
|
175
|
+
// This filter is malformed: multiple operators at the root level
|
|
176
|
+
filter: {
|
|
177
|
+
"$gt": ["$price", 50],
|
|
178
|
+
"$lt": ["$price", 200]
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
const mockLLM = {
|
|
183
|
+
stream: vi.fn().mockReturnValue(createMockStream(aiResponse)),
|
|
184
|
+
};
|
|
185
|
+
getAIProvider.mockResolvedValue(mockLLM);
|
|
186
|
+
|
|
187
|
+
// Configure le retour du mock pour ce test spécifique
|
|
188
|
+
const searchDataSpy = vi.spyOn(dataOperations, 'searchData').mockResolvedValue({ data: [], count: 0 });
|
|
189
|
+
|
|
190
|
+
const params = { message: 'cherche produits entre 50 et 200', history: [] };
|
|
191
|
+
// On peut maintenant utiliser la fonction handleChatRequest importée normalement
|
|
192
|
+
await handleChatRequest(params, testUser);
|
|
193
|
+
|
|
194
|
+
// On s'attend à ce que le filtre ait été encapsulé dans un $and
|
|
195
|
+
const expectedFilter = {
|
|
196
|
+
"$and": [
|
|
197
|
+
{ "$gt": ["$price", 50] },
|
|
198
|
+
{ "$lt": ["$price", 200] }
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
expect(searchDataSpy).toHaveBeenCalledWith(expect.objectContaining({ filter: expectedFilter }), expect.anything());
|
|
203
|
+
|
|
204
|
+
// On nettoie l'espion après le test
|
|
205
|
+
searchDataSpy.mockRestore();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// __tests__/config.test.js
|
|
2
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
3
|
+
import { Config } from '../src/config.js';
|
|
4
|
+
import { initEngine } from '../src/setenv.js';
|
|
5
|
+
|
|
6
|
+
describe('Configuration Module Tests', () => {
|
|
7
|
+
|
|
8
|
+
it('should correctly get and set a simple value', () => {
|
|
9
|
+
const key = 'testKey';
|
|
10
|
+
const value = 'testValue';
|
|
11
|
+
|
|
12
|
+
Config.Set(key, value);
|
|
13
|
+
const retrievedValue = Config.Get(key);
|
|
14
|
+
|
|
15
|
+
expect(retrievedValue).toBe(value);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should return the default value if a key is not set', () => {
|
|
19
|
+
const defaultValue = 'default';
|
|
20
|
+
const retrievedValue = Config.Get('nonExistentKey', defaultValue);
|
|
21
|
+
|
|
22
|
+
expect(retrievedValue).toBe(defaultValue);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should initialize the engine with only the modules specified in the config', async () => {
|
|
26
|
+
// 1. Définir une configuration de modules minimale.
|
|
27
|
+
const targetModules = ['mongodb', 'user'];
|
|
28
|
+
Config.Set("modules", targetModules);
|
|
29
|
+
|
|
30
|
+
// 2. Initialiser le moteur. initEngine devrait lire cette configuration.
|
|
31
|
+
const engine = await initEngine();
|
|
32
|
+
|
|
33
|
+
// 3. Vérifier que le moteur a chargé UNIQUEMENT les modules spécifiés.
|
|
34
|
+
// La propriété _modules de l'engine contient les instances des modules chargés.
|
|
35
|
+
const loadedModuleNames = engine._modules.map(m => m.module);
|
|
36
|
+
|
|
37
|
+
expect(engine._modules).toBeInstanceOf(Array);
|
|
38
|
+
expect(loadedModuleNames).toHaveLength(targetModules.length);
|
|
39
|
+
expect(loadedModuleNames).toEqual(expect.arrayContaining(targetModules));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// __tests__/data.history.integration.test.js
|
|
2
2
|
import { ObjectId } from 'mongodb';
|
|
3
3
|
import { expect, describe, it, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
4
|
-
import { handleGetHistoryRequest } from '../src/modules/data/data.history.js';
|
|
4
|
+
import { handleGetHistoryRequest, handleGetRevisionRequest, handleRevertToRevisionRequest } from '../src/modules/data/data.history.js';
|
|
5
5
|
import { Config } from '../src/config.js';
|
|
6
6
|
import { sleep } from '../src/core.js';
|
|
7
|
-
import { insertData, editData,
|
|
7
|
+
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";
|
|
@@ -16,6 +16,20 @@ let historyCollection;
|
|
|
16
16
|
let datasCollection;
|
|
17
17
|
let modelsCollection;
|
|
18
18
|
|
|
19
|
+
// Mock Express req/res objects for testing API handlers
|
|
20
|
+
const mockReq = (params = {}, query = {}, body = {}, user = testUser) => ({
|
|
21
|
+
params,
|
|
22
|
+
query,
|
|
23
|
+
body,
|
|
24
|
+
me: user
|
|
25
|
+
});
|
|
26
|
+
const mockRes = () => {
|
|
27
|
+
const res = {};
|
|
28
|
+
res.status = (code) => { res.statusCode = code; return res; };
|
|
29
|
+
res.json = (data) => { res.body = data; return res; };
|
|
30
|
+
return res;
|
|
31
|
+
};
|
|
32
|
+
|
|
19
33
|
describe('Data History Module Integration Tests', () => {
|
|
20
34
|
|
|
21
35
|
beforeAll(async () => {
|
|
@@ -88,7 +102,7 @@ describe('Data History Module Integration Tests', () => {
|
|
|
88
102
|
};
|
|
89
103
|
const insertResult = await insertData(modelName, initialData, {}, testUser);
|
|
90
104
|
expect(insertResult.success).toBe(true);
|
|
91
|
-
const docId = new ObjectId(insertResult.
|
|
105
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
92
106
|
|
|
93
107
|
// 3. Verify the history record
|
|
94
108
|
const historyRecord = await historyCollection.findOne({ documentId: new ObjectId(docId) });
|
|
@@ -136,7 +150,7 @@ describe('Data History Module Integration Tests', () => {
|
|
|
136
150
|
// 2. Insert the initial document
|
|
137
151
|
const initialData = { name: 'Selective Widget', price: 50, stock: 200, description: 'Initial description.' };
|
|
138
152
|
const insertResult = await insertData(modelName, initialData, {}, testUser);
|
|
139
|
-
const docId = new ObjectId(insertResult.
|
|
153
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
140
154
|
|
|
141
155
|
// 3. Edit the document: change one historized field and one non-historized field
|
|
142
156
|
const updateData = { price: 55.5, description: 'Updated description.' };
|
|
@@ -179,7 +193,7 @@ describe('Data History Module Integration Tests', () => {
|
|
|
179
193
|
// 2. Insert initial document
|
|
180
194
|
const initialData = { name: 'No-Op Widget', price: 10, description: 'Initial.' };
|
|
181
195
|
const insertResult = await insertData(modelName, initialData, {}, testUser);
|
|
182
|
-
const docId = new ObjectId(insertResult.
|
|
196
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
183
197
|
|
|
184
198
|
// 3. Edit ONLY a non-historized field
|
|
185
199
|
const updateData = { description: 'This change should not be recorded.' };
|
|
@@ -192,6 +206,126 @@ describe('Data History Module Integration Tests', () => {
|
|
|
192
206
|
expect(historyCount).toBe(1);
|
|
193
207
|
});
|
|
194
208
|
|
|
209
|
+
it('should create a snapshot history record on document deletion', async () => {
|
|
210
|
+
// 1. Setup model
|
|
211
|
+
const modelName = generateUniqueName('productHistoryDelete');
|
|
212
|
+
const productModelDef = {
|
|
213
|
+
name: modelName,
|
|
214
|
+
description:'',
|
|
215
|
+
_user: testUser.username,
|
|
216
|
+
history: { enabled: true },
|
|
217
|
+
fields: [{ name: 'name', type: 'string' }]
|
|
218
|
+
};
|
|
219
|
+
await modelsCollection.insertOne(productModelDef);
|
|
220
|
+
|
|
221
|
+
// 2. Insert a document
|
|
222
|
+
const insertResult = await insertData(modelName, { name: 'Document to be deleted' }, {}, testUser);
|
|
223
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
224
|
+
|
|
225
|
+
// 3. Delete the document
|
|
226
|
+
await deleteData(modelName, [docId.toString()], testUser);
|
|
227
|
+
|
|
228
|
+
// 4. Verify the new history record (v2)
|
|
229
|
+
const historyRecord = await historyCollection.findOne({ documentId: docId, version: 2 });
|
|
230
|
+
|
|
231
|
+
expect(historyRecord).not.toBeNull();
|
|
232
|
+
expect(historyRecord.operation).toBe('delete');
|
|
233
|
+
expect(historyRecord.snapshot).not.toBeNull();
|
|
234
|
+
expect(historyRecord.snapshot.name).toBe('Document to be deleted');
|
|
235
|
+
expect(historyRecord.changes).toBeUndefined();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should correctly reconstruct a document at a specific version', async () => {
|
|
239
|
+
// 1. Setup model
|
|
240
|
+
const modelName = generateUniqueName('productHistoryReconstruct');
|
|
241
|
+
const productModelDef = {
|
|
242
|
+
name: modelName,
|
|
243
|
+
_user: testUser.username,
|
|
244
|
+
description:'',
|
|
245
|
+
history: { enabled: true },
|
|
246
|
+
fields: [
|
|
247
|
+
{ name: 'name', type: 'string' },
|
|
248
|
+
{ name: 'status', type: 'string' },
|
|
249
|
+
{ name: 'count', type: 'number' }
|
|
250
|
+
]
|
|
251
|
+
};
|
|
252
|
+
await modelsCollection.insertOne(productModelDef);
|
|
253
|
+
|
|
254
|
+
// 2. Create and update document to generate history
|
|
255
|
+
const insertResult = await insertData(modelName, { name: 'Reconstruct', status: 'initial', count: 0 }, {}, testUser); // v1
|
|
256
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
257
|
+
|
|
258
|
+
await editData(modelName, { _id: docId }, { status: 'updated' }, {}, testUser); // v2
|
|
259
|
+
await editData(modelName, { _id: docId }, { count: 10 }, {}, testUser); // v3
|
|
260
|
+
|
|
261
|
+
// 3. Test reconstruction at each version via the API handler
|
|
262
|
+
// Version 1 (creation)
|
|
263
|
+
let req = mockReq({ modelName, recordId: docId.toString(), version: '1' });
|
|
264
|
+
let res = mockRes();
|
|
265
|
+
await handleGetRevisionRequest(req, res);
|
|
266
|
+
expect(res.body.success).toBe(true);
|
|
267
|
+
expect(res.body.data.name).toBe('Reconstruct');
|
|
268
|
+
expect(res.body.data.status).toBe('initial');
|
|
269
|
+
expect(res.body.data.count).toBe(0);
|
|
270
|
+
|
|
271
|
+
// Version 2 (status updated)
|
|
272
|
+
req = mockReq({ modelName, recordId: docId.toString(), version: '2' });
|
|
273
|
+
res = mockRes();
|
|
274
|
+
await handleGetRevisionRequest(req, res);
|
|
275
|
+
expect(res.body.success).toBe(true);
|
|
276
|
+
expect(res.body.data.status).toBe('updated');
|
|
277
|
+
expect(res.body.data.count).toBe(0); // count is still 0
|
|
278
|
+
|
|
279
|
+
// Version 3 (count updated)
|
|
280
|
+
req = mockReq({ modelName, recordId: docId.toString(), version: '3' });
|
|
281
|
+
res = mockRes();
|
|
282
|
+
await handleGetRevisionRequest(req, res);
|
|
283
|
+
expect(res.body.success).toBe(true);
|
|
284
|
+
expect(res.body.data.status).toBe('updated');
|
|
285
|
+
expect(res.body.data.count).toBe(10);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should revert a document to a previous version and create a new history entry', async () => {
|
|
289
|
+
// 1. Setup model
|
|
290
|
+
const modelName = generateUniqueName('productHistoryRevert');
|
|
291
|
+
const productModelDef = {
|
|
292
|
+
name: modelName,
|
|
293
|
+
_user: testUser.username,
|
|
294
|
+
description:'',
|
|
295
|
+
history: { enabled: true },
|
|
296
|
+
fields: [{ name: 'name', type: 'string' }, { name: 'status', type: 'string' }]
|
|
297
|
+
};
|
|
298
|
+
await modelsCollection.insertOne(productModelDef);
|
|
299
|
+
|
|
300
|
+
// 2. Create and update document
|
|
301
|
+
const insertResult = await insertData(modelName, { name: 'Revert Test', status: 'v1' }, {}, testUser); // v1
|
|
302
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
303
|
+
await editData(modelName, { _id: docId }, { status: 'v2' }, {}, testUser); // v2
|
|
304
|
+
|
|
305
|
+
// 3. Revert the document to version 1
|
|
306
|
+
const req = mockReq({ modelName, recordId: docId.toString(), version: '1' });
|
|
307
|
+
const res = mockRes();
|
|
308
|
+
await handleRevertToRevisionRequest(req, res);
|
|
309
|
+
|
|
310
|
+
expect(res.body.success).toBe(true);
|
|
311
|
+
expect(res.body.message).toBe("Document successfully reverted.");
|
|
312
|
+
|
|
313
|
+
// 4. Verify the current state of the document
|
|
314
|
+
const revertedDoc = await datasCollection.findOne({ _id: docId });
|
|
315
|
+
expect(revertedDoc.status).toBe('v1');
|
|
316
|
+
|
|
317
|
+
// 5. Verify that a new history entry (v3) was created for the revert action
|
|
318
|
+
const historyRecords = await historyCollection.find({ documentId: docId }).sort({ version: 1 }).toArray();
|
|
319
|
+
expect(historyRecords).toHaveLength(3);
|
|
320
|
+
|
|
321
|
+
const revertHistoryEntry = historyRecords[2]; // v3
|
|
322
|
+
expect(revertHistoryEntry.version).toBe(3);
|
|
323
|
+
expect(revertHistoryEntry.operation).toBe('update'); // A revert is an update
|
|
324
|
+
expect(revertHistoryEntry.changes).not.toBeNull();
|
|
325
|
+
expect(revertHistoryEntry.changes.status.from).toBe('v2');
|
|
326
|
+
expect(revertHistoryEntry.changes.status.to).toBe('v1');
|
|
327
|
+
});
|
|
328
|
+
|
|
195
329
|
it('should filter history records by date range', async () => {
|
|
196
330
|
// 1. Setup model
|
|
197
331
|
const modelName = generateUniqueName('productHistoryDateFilter');
|
|
@@ -206,7 +340,7 @@ describe('Data History Module Integration Tests', () => {
|
|
|
206
340
|
|
|
207
341
|
// 2. Insert initial document
|
|
208
342
|
const insertResult = await insertData(modelName, { name: 'Time-traveling Widget' }, {}, testUser);
|
|
209
|
-
const docId = new ObjectId(insertResult.
|
|
343
|
+
const docId = new ObjectId(insertResult.data._id);
|
|
210
344
|
|
|
211
345
|
// 3. Create history entries at different times
|
|
212
346
|
// To simulate different timestamps, we'll manually insert history records
|
|
@@ -227,21 +361,11 @@ describe('Data History Module Integration Tests', () => {
|
|
|
227
361
|
});
|
|
228
362
|
|
|
229
363
|
// Mock Express req/res objects
|
|
230
|
-
|
|
231
|
-
params: { modelName, recordId: docId.toString() },
|
|
232
|
-
query,
|
|
233
|
-
me: testUser
|
|
234
|
-
});
|
|
235
|
-
const mockRes = () => {
|
|
236
|
-
const res = {};
|
|
237
|
-
res.status = (code) => { res.statusCode = code; return res; };
|
|
238
|
-
res.json = (data) => { res.body = data; return res; };
|
|
239
|
-
return res;
|
|
240
|
-
};
|
|
364
|
+
// Using the global mockReq and mockRes helpers now
|
|
241
365
|
|
|
242
366
|
// 4. Test cases
|
|
243
367
|
// Case A: Filter for February
|
|
244
|
-
let req = mockReq({ startDate: '2023-02-01', endDate: '2023-02-28' });
|
|
368
|
+
let req = mockReq({ modelName, recordId: docId.toString() }, { startDate: '2023-02-01', endDate: '2023-02-28' });
|
|
245
369
|
let res = mockRes();
|
|
246
370
|
await handleGetHistoryRequest(req, res);
|
|
247
371
|
expect(res.body.success).toBe(true);
|
|
@@ -249,14 +373,14 @@ describe('Data History Module Integration Tests', () => {
|
|
|
249
373
|
expect(res.body.data.map(d => d._v).sort()).toEqual([2, 3]);
|
|
250
374
|
|
|
251
375
|
// Case B: Filter starting from Feb 15th
|
|
252
|
-
req = mockReq({ startDate: '2023-02-15' });
|
|
376
|
+
req = mockReq({ modelName, recordId: docId.toString() }, { startDate: '2023-02-15' });
|
|
253
377
|
res = mockRes();
|
|
254
378
|
await handleGetHistoryRequest(req, res);
|
|
255
379
|
expect(res.body.success).toBe(true);
|
|
256
380
|
expect(res.body.count).toBe(3); // v2, v3, v4
|
|
257
381
|
|
|
258
382
|
// Case C: Filter up to Feb 15th (inclusive)
|
|
259
|
-
req = mockReq({ endDate: '2023-02-15' });
|
|
383
|
+
req = mockReq({ modelName, recordId: docId.toString() }, { endDate: '2023-02-15' });
|
|
260
384
|
res = mockRes();
|
|
261
385
|
await handleGetHistoryRequest(req, res);
|
|
262
386
|
expect(res.body.success).toBe(true);
|