@objectql/cli 4.0.0 → 4.0.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/package.json +7 -7
- package/templates/hello-world/CHANGELOG.md +16 -0
- package/templates/hello-world/README.md +73 -10
- package/templates/hello-world/package.json +1 -1
- package/templates/hello-world/src/index.ts +17 -5
- package/templates/starter/CHANGELOG.md +20 -0
- package/templates/starter/package.json +1 -1
- package/templates/starter/src/modules/projects/projects.action.ts +195 -346
- package/templates/starter/src/modules/projects/projects.hook.ts +98 -263
- package/templates/starter/src/modules/projects/projects.object.yml +65 -6
- package/templates/starter/src/modules/projects/projects.validation.yml +13 -4
- package/templates/starter/src/seed.ts +1 -1
- package/templates/starter/tsconfig.tsbuildinfo +1 -1
- package/templates/starter/__tests__/projects-hooks-actions.test.ts +0 -498
|
@@ -1,498 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectQL
|
|
3
|
-
* Copyright (c) 2026-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Comprehensive Test Suite for Project Hooks and Actions
|
|
11
|
-
*
|
|
12
|
-
* This test file demonstrates and validates:
|
|
13
|
-
* 1. All hook types (beforeCreate, afterCreate, beforeUpdate, etc.)
|
|
14
|
-
* 2. All action types (record actions and global actions)
|
|
15
|
-
* 3. Business logic patterns from the specification
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { ObjectQL } from '@objectql/core';
|
|
19
|
-
import hooks from '../src/modules/projects/projects.hook';
|
|
20
|
-
import * as actions from '../src/modules/projects/projects.action';
|
|
21
|
-
|
|
22
|
-
describe('Project Hooks - Comprehensive Examples', () => {
|
|
23
|
-
let app: ObjectQL;
|
|
24
|
-
|
|
25
|
-
beforeEach(async () => {
|
|
26
|
-
// Use in-memory driver for testing
|
|
27
|
-
app = new ObjectQL({
|
|
28
|
-
datasources: {
|
|
29
|
-
default: {
|
|
30
|
-
find: jest.fn().mockResolvedValue([]),
|
|
31
|
-
findOne: jest.fn().mockResolvedValue(null),
|
|
32
|
-
create: jest.fn((obj, data) => ({ ...data, _id: 'test-id' })),
|
|
33
|
-
update: jest.fn((obj, id, data) => data),
|
|
34
|
-
delete: jest.fn().mockResolvedValue(true),
|
|
35
|
-
count: jest.fn().mockResolvedValue(0)
|
|
36
|
-
} as any
|
|
37
|
-
},
|
|
38
|
-
objects: {
|
|
39
|
-
'projects': {
|
|
40
|
-
name: 'projects',
|
|
41
|
-
fields: {
|
|
42
|
-
name: { type: 'text' },
|
|
43
|
-
status: { type: 'text' },
|
|
44
|
-
budget: { type: 'number' },
|
|
45
|
-
owner: { type: 'text' }
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
await app.init();
|
|
51
|
-
|
|
52
|
-
// Register hooks
|
|
53
|
-
if (hooks.beforeCreate) app.on('beforeCreate', 'projects', hooks.beforeCreate);
|
|
54
|
-
if (hooks.afterCreate) app.on('afterCreate', 'projects', hooks.afterCreate);
|
|
55
|
-
if (hooks.beforeFind) app.on('beforeFind', 'projects', hooks.beforeFind);
|
|
56
|
-
if (hooks.afterFind) app.on('afterFind', 'projects', hooks.afterFind);
|
|
57
|
-
if (hooks.beforeUpdate) app.on('beforeUpdate', 'projects', hooks.beforeUpdate);
|
|
58
|
-
if (hooks.afterUpdate) app.on('afterUpdate', 'projects', hooks.afterUpdate);
|
|
59
|
-
if (hooks.beforeDelete) app.on('beforeDelete', 'projects', hooks.beforeDelete);
|
|
60
|
-
if (hooks.afterDelete) app.on('afterDelete', 'projects', hooks.afterDelete);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('beforeCreate Hook', () => {
|
|
64
|
-
it('should auto-assign owner from user context', async () => {
|
|
65
|
-
const repo = app.createContext({ userId: 'user123' }).object('projects');
|
|
66
|
-
|
|
67
|
-
await repo.create({ name: 'Test Project' });
|
|
68
|
-
|
|
69
|
-
const driver = app.datasource('default');
|
|
70
|
-
expect(driver.create).toHaveBeenCalledWith(
|
|
71
|
-
'projects',
|
|
72
|
-
expect.objectContaining({
|
|
73
|
-
name: 'Test Project',
|
|
74
|
-
owner: 'user123'
|
|
75
|
-
}),
|
|
76
|
-
expect.any(Object)
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should set default status to planned', async () => {
|
|
81
|
-
const repo = app.createContext({}).object('projects');
|
|
82
|
-
|
|
83
|
-
await repo.create({ name: 'Test Project' });
|
|
84
|
-
|
|
85
|
-
const driver = app.datasource('default');
|
|
86
|
-
expect(driver.create).toHaveBeenCalledWith(
|
|
87
|
-
'projects',
|
|
88
|
-
expect.objectContaining({
|
|
89
|
-
status: 'planned'
|
|
90
|
-
}),
|
|
91
|
-
expect.any(Object)
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should validate project name is required', async () => {
|
|
96
|
-
const repo = app.createContext({}).object('projects');
|
|
97
|
-
|
|
98
|
-
await expect(repo.create({ name: '' }))
|
|
99
|
-
.rejects
|
|
100
|
-
.toThrow('Project name is required');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should validate project name length', async () => {
|
|
104
|
-
const repo = app.createContext({}).object('projects');
|
|
105
|
-
const longName = 'a'.repeat(101);
|
|
106
|
-
|
|
107
|
-
await expect(repo.create({ name: longName }))
|
|
108
|
-
.rejects
|
|
109
|
-
.toThrow('Project name must be 100 characters or less');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should set default budget to 0', async () => {
|
|
113
|
-
const repo = app.createContext({}).object('projects');
|
|
114
|
-
|
|
115
|
-
await repo.create({ name: 'Test Project' });
|
|
116
|
-
|
|
117
|
-
const driver = app.datasource('default');
|
|
118
|
-
expect(driver.create).toHaveBeenCalledWith(
|
|
119
|
-
'projects',
|
|
120
|
-
expect.objectContaining({
|
|
121
|
-
budget: 0
|
|
122
|
-
}),
|
|
123
|
-
expect.any(Object)
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('beforeUpdate Hook', () => {
|
|
129
|
-
it('should validate budget is not negative', async () => {
|
|
130
|
-
const repo = app.createContext({}).object('projects');
|
|
131
|
-
const driver = app.datasource('default');
|
|
132
|
-
|
|
133
|
-
// Mock existing project
|
|
134
|
-
(driver.findOne as jest.Mock).mockResolvedValueOnce({
|
|
135
|
-
_id: '1',
|
|
136
|
-
name: 'Test',
|
|
137
|
-
status: 'planned',
|
|
138
|
-
budget: 1000
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
await expect(repo.update('1', { budget: -100 }))
|
|
142
|
-
.rejects
|
|
143
|
-
.toThrow('Budget cannot be negative');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should prevent invalid status transitions', async () => {
|
|
147
|
-
const repo = app.createContext({}).object('projects');
|
|
148
|
-
const driver = app.datasource('default');
|
|
149
|
-
|
|
150
|
-
// Mock completed project
|
|
151
|
-
(driver.findOne as jest.Mock).mockResolvedValueOnce({
|
|
152
|
-
_id: '1',
|
|
153
|
-
name: 'Test',
|
|
154
|
-
status: 'completed',
|
|
155
|
-
budget: 1000
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
await expect(repo.update('1', { status: 'planned' }))
|
|
159
|
-
.rejects
|
|
160
|
-
.toThrow('Invalid status transition');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should allow valid status transition from planned to in_progress', async () => {
|
|
164
|
-
const repo = app.createContext({}).object('projects');
|
|
165
|
-
const driver = app.datasource('default');
|
|
166
|
-
|
|
167
|
-
(driver.findOne as jest.Mock).mockResolvedValueOnce({
|
|
168
|
-
_id: '1',
|
|
169
|
-
name: 'Test',
|
|
170
|
-
status: 'planned',
|
|
171
|
-
budget: 1000
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
await repo.update('1', { status: 'in_progress' });
|
|
175
|
-
|
|
176
|
-
expect(driver.update).toHaveBeenCalled();
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('should require end_date when marking as completed', async () => {
|
|
180
|
-
const repo = app.createContext({}).object('projects');
|
|
181
|
-
const driver = app.datasource('default');
|
|
182
|
-
|
|
183
|
-
(driver.findOne as jest.Mock).mockResolvedValueOnce({
|
|
184
|
-
_id: '1',
|
|
185
|
-
name: 'Test',
|
|
186
|
-
status: 'in_progress',
|
|
187
|
-
budget: 1000
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
await expect(repo.update('1', { status: 'completed' }))
|
|
191
|
-
.rejects
|
|
192
|
-
.toThrow('End date is required when completing a project');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('beforeDelete Hook', () => {
|
|
197
|
-
it('should prevent deletion of completed projects', async () => {
|
|
198
|
-
const repo = app.createContext({}).object('projects');
|
|
199
|
-
const driver = app.datasource('default');
|
|
200
|
-
|
|
201
|
-
(driver.findOne as jest.Mock).mockResolvedValueOnce({
|
|
202
|
-
_id: '1',
|
|
203
|
-
name: 'Test',
|
|
204
|
-
status: 'completed'
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
await expect(repo.delete('1'))
|
|
208
|
-
.rejects
|
|
209
|
-
.toThrow('Cannot delete completed projects');
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
describe('afterFind Hook', () => {
|
|
214
|
-
it('should add computed progress field', async () => {
|
|
215
|
-
const repo = app.createContext({}).object('projects');
|
|
216
|
-
const driver = app.datasource('default');
|
|
217
|
-
|
|
218
|
-
(driver.find as jest.Mock).mockResolvedValueOnce([
|
|
219
|
-
{ name: 'Project 1', status: 'planned' },
|
|
220
|
-
{ name: 'Project 2', status: 'in_progress' },
|
|
221
|
-
{ name: 'Project 3', status: 'completed' }
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
const results = await repo.find({});
|
|
225
|
-
|
|
226
|
-
expect(results[0].progress).toBe(0);
|
|
227
|
-
expect(results[1].progress).toBe(50);
|
|
228
|
-
expect(results[2].progress).toBe(100);
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
describe('Project Actions - Comprehensive Examples', () => {
|
|
234
|
-
let mockApi: any;
|
|
235
|
-
let mockUser: any;
|
|
236
|
-
|
|
237
|
-
beforeEach(() => {
|
|
238
|
-
mockUser = { id: 'user123', isAdmin: false };
|
|
239
|
-
mockApi = {
|
|
240
|
-
findOne: jest.fn(),
|
|
241
|
-
find: jest.fn(),
|
|
242
|
-
create: jest.fn(),
|
|
243
|
-
update: jest.fn(),
|
|
244
|
-
delete: jest.fn(),
|
|
245
|
-
count: jest.fn()
|
|
246
|
-
};
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe('complete - Record Action', () => {
|
|
250
|
-
it('should complete a project successfully', async () => {
|
|
251
|
-
const mockProject = {
|
|
252
|
-
_id: 'proj1',
|
|
253
|
-
name: 'Test Project',
|
|
254
|
-
status: 'in_progress',
|
|
255
|
-
description: 'Original description'
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
mockApi.findOne.mockResolvedValue(mockProject);
|
|
259
|
-
mockApi.update.mockResolvedValue({ ...mockProject, status: 'completed' });
|
|
260
|
-
|
|
261
|
-
const result = await actions.complete.handler({
|
|
262
|
-
objectName: 'projects',
|
|
263
|
-
actionName: 'complete',
|
|
264
|
-
id: 'proj1',
|
|
265
|
-
input: { comment: 'All tasks done' },
|
|
266
|
-
api: mockApi,
|
|
267
|
-
user: mockUser
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
expect(result.success).toBe(true);
|
|
271
|
-
expect(result.message).toContain('completed successfully');
|
|
272
|
-
expect(mockApi.update).toHaveBeenCalledWith(
|
|
273
|
-
'projects',
|
|
274
|
-
'proj1',
|
|
275
|
-
expect.objectContaining({
|
|
276
|
-
status: 'completed'
|
|
277
|
-
})
|
|
278
|
-
);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('should reject if project is already completed', async () => {
|
|
282
|
-
mockApi.findOne.mockResolvedValue({
|
|
283
|
-
_id: 'proj1',
|
|
284
|
-
status: 'completed'
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
await expect(actions.complete.handler({
|
|
288
|
-
objectName: 'projects',
|
|
289
|
-
actionName: 'complete',
|
|
290
|
-
id: 'proj1',
|
|
291
|
-
input: {},
|
|
292
|
-
api: mockApi,
|
|
293
|
-
user: mockUser
|
|
294
|
-
})).rejects.toThrow('already completed');
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe('approve - Record Action', () => {
|
|
299
|
-
it('should approve a planned project', async () => {
|
|
300
|
-
mockApi.findOne.mockResolvedValue({
|
|
301
|
-
_id: 'proj1',
|
|
302
|
-
name: 'Test Project',
|
|
303
|
-
status: 'planned',
|
|
304
|
-
budget: 50000
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const result = await actions.approve.handler({
|
|
308
|
-
objectName: 'projects',
|
|
309
|
-
actionName: 'approve',
|
|
310
|
-
id: 'proj1',
|
|
311
|
-
input: { comment: 'Looks good' },
|
|
312
|
-
api: mockApi,
|
|
313
|
-
user: mockUser
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
expect(result.success).toBe(true);
|
|
317
|
-
expect(result.new_status).toBe('in_progress');
|
|
318
|
-
expect(mockApi.update).toHaveBeenCalledWith(
|
|
319
|
-
'projects',
|
|
320
|
-
'proj1',
|
|
321
|
-
expect.objectContaining({
|
|
322
|
-
status: 'in_progress',
|
|
323
|
-
approved_by: 'user123'
|
|
324
|
-
})
|
|
325
|
-
);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('should require approval comment', async () => {
|
|
329
|
-
await expect(actions.approve.handler({
|
|
330
|
-
objectName: 'projects',
|
|
331
|
-
actionName: 'approve',
|
|
332
|
-
id: 'proj1',
|
|
333
|
-
input: { comment: '' },
|
|
334
|
-
api: mockApi,
|
|
335
|
-
user: mockUser
|
|
336
|
-
})).rejects.toThrow('Approval comment is required');
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
describe('clone - Record Action', () => {
|
|
341
|
-
it('should clone a project with new name', async () => {
|
|
342
|
-
const sourceProject = {
|
|
343
|
-
_id: 'proj1',
|
|
344
|
-
name: 'Original Project',
|
|
345
|
-
description: 'Original description',
|
|
346
|
-
status: 'in_progress',
|
|
347
|
-
priority: 'high',
|
|
348
|
-
budget: 10000,
|
|
349
|
-
owner: 'owner123'
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
mockApi.findOne.mockResolvedValue(sourceProject);
|
|
353
|
-
mockApi.create.mockResolvedValue({ _id: 'proj2', name: 'Cloned Project' });
|
|
354
|
-
|
|
355
|
-
const result = await actions.clone.handler({
|
|
356
|
-
objectName: 'projects',
|
|
357
|
-
actionName: 'clone',
|
|
358
|
-
id: 'proj1',
|
|
359
|
-
input: { new_name: 'Cloned Project', copy_tasks: false },
|
|
360
|
-
api: mockApi,
|
|
361
|
-
user: mockUser
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
expect(result.success).toBe(true);
|
|
365
|
-
expect(result.new_project_id).toBe('proj2');
|
|
366
|
-
expect(mockApi.create).toHaveBeenCalledWith(
|
|
367
|
-
'projects',
|
|
368
|
-
expect.objectContaining({
|
|
369
|
-
name: 'Cloned Project',
|
|
370
|
-
status: 'planned', // Reset to planned
|
|
371
|
-
priority: 'high',
|
|
372
|
-
budget: 10000,
|
|
373
|
-
owner: 'user123' // Assigned to current user
|
|
374
|
-
})
|
|
375
|
-
);
|
|
376
|
-
});
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
describe('import_projects - Global Action', () => {
|
|
380
|
-
it('should import multiple projects successfully', async () => {
|
|
381
|
-
const projectsData = [
|
|
382
|
-
{ name: 'Project 1', description: 'Desc 1' },
|
|
383
|
-
{ name: 'Project 2', description: 'Desc 2', status: 'in_progress' }
|
|
384
|
-
];
|
|
385
|
-
|
|
386
|
-
mockApi.create.mockResolvedValue({ _id: 'new-id' });
|
|
387
|
-
|
|
388
|
-
const result = await actions.import_projects.handler({
|
|
389
|
-
objectName: 'projects',
|
|
390
|
-
actionName: 'import_projects',
|
|
391
|
-
input: { source: 'json', data: projectsData },
|
|
392
|
-
api: mockApi,
|
|
393
|
-
user: mockUser
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
expect(result.success).toBe(true);
|
|
397
|
-
expect(result.successCount).toBe(2);
|
|
398
|
-
expect(mockApi.create).toHaveBeenCalledTimes(2);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('should collect errors for invalid projects', async () => {
|
|
402
|
-
const projectsData = [
|
|
403
|
-
{ name: 'Valid Project' },
|
|
404
|
-
{ description: 'Missing name' }, // Invalid - no name
|
|
405
|
-
{ name: 'Another Valid' }
|
|
406
|
-
];
|
|
407
|
-
|
|
408
|
-
mockApi.create.mockResolvedValue({ _id: 'new-id' });
|
|
409
|
-
|
|
410
|
-
const result = await actions.import_projects.handler({
|
|
411
|
-
objectName: 'projects',
|
|
412
|
-
actionName: 'import_projects',
|
|
413
|
-
input: { source: 'json', data: projectsData },
|
|
414
|
-
api: mockApi,
|
|
415
|
-
user: mockUser
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
expect(result.failed).toBe(1);
|
|
419
|
-
expect(result.errors).toHaveLength(1);
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
describe('bulk_update_status - Global Action', () => {
|
|
424
|
-
it('should update multiple projects status', async () => {
|
|
425
|
-
mockApi.findOne.mockImplementation((obj: string, id: string) => ({
|
|
426
|
-
_id: id,
|
|
427
|
-
name: `Project ${id}`,
|
|
428
|
-
status: 'planned'
|
|
429
|
-
}));
|
|
430
|
-
|
|
431
|
-
const result = await actions.bulk_update_status.handler({
|
|
432
|
-
objectName: 'projects',
|
|
433
|
-
actionName: 'bulk_update_status',
|
|
434
|
-
input: {
|
|
435
|
-
project_ids: ['proj1', 'proj2', 'proj3'],
|
|
436
|
-
new_status: 'in_progress'
|
|
437
|
-
},
|
|
438
|
-
api: mockApi,
|
|
439
|
-
user: mockUser
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
expect(result.updated).toBe(3);
|
|
443
|
-
expect(mockApi.update).toHaveBeenCalledTimes(3);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('should skip completed projects', async () => {
|
|
447
|
-
mockApi.findOne.mockImplementation((obj: string, id: string) => {
|
|
448
|
-
if (id === 'proj2') {
|
|
449
|
-
return { _id: id, name: 'Completed Project', status: 'completed' };
|
|
450
|
-
}
|
|
451
|
-
return { _id: id, name: 'Project', status: 'planned' };
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
const result = await actions.bulk_update_status.handler({
|
|
455
|
-
objectName: 'projects',
|
|
456
|
-
actionName: 'bulk_update_status',
|
|
457
|
-
input: {
|
|
458
|
-
project_ids: ['proj1', 'proj2', 'proj3'],
|
|
459
|
-
new_status: 'planned'
|
|
460
|
-
},
|
|
461
|
-
api: mockApi,
|
|
462
|
-
user: mockUser
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
expect(result.updated).toBe(2);
|
|
466
|
-
expect(result.skipped).toBe(1);
|
|
467
|
-
});
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
describe('generate_report - Global Action', () => {
|
|
471
|
-
it('should generate statistical report', async () => {
|
|
472
|
-
const mockProjects = [
|
|
473
|
-
{ name: 'P1', status: 'planned', priority: 'high', budget: 10000 },
|
|
474
|
-
{ name: 'P2', status: 'in_progress', priority: 'normal', budget: 20000 },
|
|
475
|
-
{ name: 'P3', status: 'completed', priority: 'low', budget: 15000 },
|
|
476
|
-
{ name: 'P4', status: 'planned', priority: 'normal', budget: 5000 }
|
|
477
|
-
];
|
|
478
|
-
|
|
479
|
-
mockApi.find.mockResolvedValue(mockProjects);
|
|
480
|
-
|
|
481
|
-
const result = await actions.generate_report.handler({
|
|
482
|
-
objectName: 'projects',
|
|
483
|
-
actionName: 'generate_report',
|
|
484
|
-
input: {},
|
|
485
|
-
api: mockApi,
|
|
486
|
-
user: mockUser
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
expect(result.success).toBe(true);
|
|
490
|
-
expect(result.report.total_projects).toBe(4);
|
|
491
|
-
expect(result.report.by_status.planned).toBe(2);
|
|
492
|
-
expect(result.report.by_status.in_progress).toBe(1);
|
|
493
|
-
expect(result.report.by_status.completed).toBe(1);
|
|
494
|
-
expect(result.report.total_budget).toBe(50000);
|
|
495
|
-
expect(result.report.average_budget).toBe(12500);
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
});
|