@flusys/nestjs-form-builder 6.0.2 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/controllers/form-result.controller.spec.js +257 -0
- package/cjs/controllers/form.controller.spec.js +212 -0
- package/cjs/docs/form-builder-swagger.config.spec.js +75 -0
- package/cjs/entities/index.spec.js +18 -0
- package/cjs/modules/form-builder.module.spec.js +133 -0
- package/cjs/services/form-builder-config.service.spec.js +121 -0
- package/cjs/services/form-builder-datasource.provider.spec.js +147 -0
- package/cjs/services/form-result.service.spec.js +702 -0
- package/cjs/services/form.service.spec.js +746 -0
- package/cjs/utils/computed-field.utils.spec.js +1205 -0
- package/cjs/utils/permission.utils.spec.js +118 -0
- package/fesm/controllers/form-result.controller.spec.js +253 -0
- package/fesm/controllers/form.controller.spec.js +208 -0
- package/fesm/docs/form-builder-swagger.config.spec.js +71 -0
- package/fesm/entities/index.spec.js +14 -0
- package/fesm/modules/form-builder.module.spec.js +129 -0
- package/fesm/services/form-builder-config.service.spec.js +117 -0
- package/fesm/services/form-builder-datasource.provider.spec.js +143 -0
- package/fesm/services/form-result.service.spec.js +657 -0
- package/fesm/services/form.service.spec.js +701 -0
- package/fesm/utils/computed-field.utils.spec.js +1201 -0
- package/fesm/utils/permission.utils.spec.js +114 -0
- package/package.json +3 -3
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _testing = require("@nestjs/testing");
|
|
6
|
+
const _nestjscore = require("@flusys/nestjs-core");
|
|
7
|
+
const _modules = require("@flusys/nestjs-shared/modules");
|
|
8
|
+
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
9
|
+
const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
10
|
+
const _entities = require("../entities");
|
|
11
|
+
const _formaccesstypeenum = require("../enums/form-access-type.enum");
|
|
12
|
+
const _permissionutils = /*#__PURE__*/ _interop_require_wildcard(require("../utils/permission.utils"));
|
|
13
|
+
const _formservice = require("./form.service");
|
|
14
|
+
const _formbuilderconfigservice = require("./form-builder-config.service");
|
|
15
|
+
const _formbuilderdatasourceprovider = require("./form-builder-datasource.provider");
|
|
16
|
+
function _getRequireWildcardCache(nodeInterop) {
|
|
17
|
+
if (typeof WeakMap !== "function") return null;
|
|
18
|
+
var cacheBabelInterop = new WeakMap();
|
|
19
|
+
var cacheNodeInterop = new WeakMap();
|
|
20
|
+
return (_getRequireWildcardCache = function(nodeInterop) {
|
|
21
|
+
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
|
|
22
|
+
})(nodeInterop);
|
|
23
|
+
}
|
|
24
|
+
function _interop_require_wildcard(obj, nodeInterop) {
|
|
25
|
+
if (!nodeInterop && obj && obj.__esModule) {
|
|
26
|
+
return obj;
|
|
27
|
+
}
|
|
28
|
+
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
|
|
29
|
+
return {
|
|
30
|
+
default: obj
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
var cache = _getRequireWildcardCache(nodeInterop);
|
|
34
|
+
if (cache && cache.has(obj)) {
|
|
35
|
+
return cache.get(obj);
|
|
36
|
+
}
|
|
37
|
+
var newObj = {
|
|
38
|
+
__proto__: null
|
|
39
|
+
};
|
|
40
|
+
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
|
|
41
|
+
for(var key in obj){
|
|
42
|
+
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
43
|
+
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
|
|
44
|
+
if (desc && (desc.get || desc.set)) {
|
|
45
|
+
Object.defineProperty(newObj, key, desc);
|
|
46
|
+
} else {
|
|
47
|
+
newObj[key] = obj[key];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
newObj.default = obj;
|
|
52
|
+
if (cache) {
|
|
53
|
+
cache.set(obj, newObj);
|
|
54
|
+
}
|
|
55
|
+
return newObj;
|
|
56
|
+
}
|
|
57
|
+
jest.mock('../utils/permission.utils', ()=>({
|
|
58
|
+
validateUserPermissions: jest.fn()
|
|
59
|
+
}));
|
|
60
|
+
function buildForm(overrides = {}) {
|
|
61
|
+
return {
|
|
62
|
+
id: 'form-uuid-1',
|
|
63
|
+
name: 'Customer Survey',
|
|
64
|
+
description: null,
|
|
65
|
+
slug: 'customer-survey',
|
|
66
|
+
schema: {
|
|
67
|
+
sections: [],
|
|
68
|
+
settings: {}
|
|
69
|
+
},
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
accessType: _formaccesstypeenum.FormAccessType.AUTHENTICATED,
|
|
72
|
+
actionGroups: null,
|
|
73
|
+
tags: null,
|
|
74
|
+
isActive: true,
|
|
75
|
+
createdAt: new Date(),
|
|
76
|
+
updatedAt: new Date(),
|
|
77
|
+
deletedAt: null,
|
|
78
|
+
createdById: null,
|
|
79
|
+
updatedById: null,
|
|
80
|
+
deletedById: null,
|
|
81
|
+
...overrides
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
describe('FormService', ()=>{
|
|
85
|
+
let service;
|
|
86
|
+
let mockRepo;
|
|
87
|
+
let mockFormBuilderConfig;
|
|
88
|
+
async function buildService(companyFeatureEnabled = false) {
|
|
89
|
+
mockRepo = (0, _repositorymock.createMockRepository)();
|
|
90
|
+
const mockDataSourceProvider = (0, _repositorymock.createMockDataSourceProvider)(mockRepo);
|
|
91
|
+
mockFormBuilderConfig = {
|
|
92
|
+
isCompanyFeatureEnabled: jest.fn().mockReturnValue(companyFeatureEnabled)
|
|
93
|
+
};
|
|
94
|
+
const module = await _testing.Test.createTestingModule({
|
|
95
|
+
providers: [
|
|
96
|
+
_formservice.FormService,
|
|
97
|
+
{
|
|
98
|
+
provide: 'CACHE_INSTANCE',
|
|
99
|
+
useValue: {}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
provide: _modules.UtilsService,
|
|
103
|
+
useValue: {}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
provide: _formbuilderconfigservice.FormBuilderConfigService,
|
|
107
|
+
useValue: mockFormBuilderConfig
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
provide: _formbuilderdatasourceprovider.FormBuilderDataSourceProvider,
|
|
111
|
+
useValue: mockDataSourceProvider
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}).compile();
|
|
115
|
+
service = await module.resolve(_formservice.FormService);
|
|
116
|
+
// Pre-warm the lazily-initialized repository so protected/public helper methods
|
|
117
|
+
// that read `this.repository` directly (outside the insert/update pipeline) work in tests.
|
|
118
|
+
await service.ensureDataSourceRepository();
|
|
119
|
+
}
|
|
120
|
+
afterEach(()=>{
|
|
121
|
+
jest.clearAllMocks();
|
|
122
|
+
});
|
|
123
|
+
describe('resolveEntity', ()=>{
|
|
124
|
+
it('resolves to Form when company feature disabled', async ()=>{
|
|
125
|
+
await buildService(false);
|
|
126
|
+
expect(service.resolveEntity()).toBe(_entities.Form);
|
|
127
|
+
});
|
|
128
|
+
it('resolves to FormWithCompany when company feature enabled', async ()=>{
|
|
129
|
+
await buildService(true);
|
|
130
|
+
expect(service.resolveEntity()).toBe(_entities.FormWithCompany);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// ==========================================================================
|
|
134
|
+
// convertSingleDtoToEntity — schema versioning + company scoping
|
|
135
|
+
// ==========================================================================
|
|
136
|
+
describe('convertSingleDtoToEntity', ()=>{
|
|
137
|
+
describe('create (no id)', ()=>{
|
|
138
|
+
it('merges the DTO fields into a fresh entity without touching the repository', async ()=>{
|
|
139
|
+
await buildService(false);
|
|
140
|
+
const result = await service.convertSingleDtoToEntity({
|
|
141
|
+
name: 'New Form',
|
|
142
|
+
schema: {
|
|
143
|
+
sections: []
|
|
144
|
+
}
|
|
145
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
146
|
+
expect(result.name).toBe('New Form');
|
|
147
|
+
expect(mockRepo.findOne).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
it('sets companyId from the DTO when company feature enabled and DTO provides it', async ()=>{
|
|
150
|
+
await buildService(true);
|
|
151
|
+
const result = await service.convertSingleDtoToEntity({
|
|
152
|
+
name: 'New Form',
|
|
153
|
+
schema: {},
|
|
154
|
+
companyId: 'company-explicit'
|
|
155
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
156
|
+
companyId: 'company-from-user'
|
|
157
|
+
}));
|
|
158
|
+
expect(result.companyId).toBe('company-explicit');
|
|
159
|
+
});
|
|
160
|
+
it('falls back to the user companyId when company feature enabled and DTO omits it', async ()=>{
|
|
161
|
+
await buildService(true);
|
|
162
|
+
const result = await service.convertSingleDtoToEntity({
|
|
163
|
+
name: 'New Form',
|
|
164
|
+
schema: {}
|
|
165
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
166
|
+
companyId: 'company-from-user'
|
|
167
|
+
}));
|
|
168
|
+
expect(result.companyId).toBe('company-from-user');
|
|
169
|
+
});
|
|
170
|
+
it('sets companyId to null when company feature enabled, no DTO value, and no user', async ()=>{
|
|
171
|
+
await buildService(true);
|
|
172
|
+
const result = await service.convertSingleDtoToEntity({
|
|
173
|
+
name: 'New Form',
|
|
174
|
+
schema: {}
|
|
175
|
+
}, null);
|
|
176
|
+
expect(result.companyId).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
it('does not set companyId when company feature is disabled', async ()=>{
|
|
179
|
+
await buildService(false);
|
|
180
|
+
const result = await service.convertSingleDtoToEntity({
|
|
181
|
+
name: 'New Form',
|
|
182
|
+
schema: {}
|
|
183
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
184
|
+
companyId: 'company-from-user'
|
|
185
|
+
}));
|
|
186
|
+
expect(result.companyId).toBeUndefined();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('update (with id)', ()=>{
|
|
190
|
+
it('throws NotFoundException when the form does not exist', async ()=>{
|
|
191
|
+
await buildService(false);
|
|
192
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
193
|
+
await expect(service.convertSingleDtoToEntity({
|
|
194
|
+
id: 'missing-id',
|
|
195
|
+
schema: {}
|
|
196
|
+
}, (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
|
|
197
|
+
response: expect.objectContaining({
|
|
198
|
+
messageKey: 'form.not.found'
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
it('bumps schemaVersion when the schema payload differs from the stored schema', async ()=>{
|
|
203
|
+
await buildService(false);
|
|
204
|
+
const existing = buildForm({
|
|
205
|
+
id: 'form-1',
|
|
206
|
+
schemaVersion: 3,
|
|
207
|
+
schema: {
|
|
208
|
+
a: 1
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
212
|
+
const result = await service.convertSingleDtoToEntity({
|
|
213
|
+
id: 'form-1',
|
|
214
|
+
schema: {
|
|
215
|
+
a: 2
|
|
216
|
+
}
|
|
217
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
218
|
+
expect(result.schemaVersion).toBe(4);
|
|
219
|
+
});
|
|
220
|
+
it('does not bump schemaVersion when the schema payload is unchanged', async ()=>{
|
|
221
|
+
await buildService(false);
|
|
222
|
+
const existing = buildForm({
|
|
223
|
+
id: 'form-1',
|
|
224
|
+
schemaVersion: 3,
|
|
225
|
+
schema: {
|
|
226
|
+
a: 1
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
230
|
+
const result = await service.convertSingleDtoToEntity({
|
|
231
|
+
id: 'form-1',
|
|
232
|
+
schema: {
|
|
233
|
+
a: 1
|
|
234
|
+
}
|
|
235
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
236
|
+
expect(result.schemaVersion).toBe(3);
|
|
237
|
+
});
|
|
238
|
+
it('does not bump schemaVersion when the DTO omits schema entirely', async ()=>{
|
|
239
|
+
await buildService(false);
|
|
240
|
+
const existing = buildForm({
|
|
241
|
+
id: 'form-1',
|
|
242
|
+
schemaVersion: 3,
|
|
243
|
+
schema: {
|
|
244
|
+
a: 1
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
248
|
+
const result = await service.convertSingleDtoToEntity({
|
|
249
|
+
id: 'form-1',
|
|
250
|
+
name: 'Renamed'
|
|
251
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
252
|
+
expect(result.schemaVersion).toBe(3);
|
|
253
|
+
expect(result.name).toBe('Renamed');
|
|
254
|
+
});
|
|
255
|
+
it('uses the DTO companyId when explicitly provided on update (company feature enabled)', async ()=>{
|
|
256
|
+
await buildService(true);
|
|
257
|
+
const existing = buildForm({
|
|
258
|
+
id: 'form-1'
|
|
259
|
+
});
|
|
260
|
+
existing.companyId = 'company-original';
|
|
261
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
262
|
+
const result = await service.convertSingleDtoToEntity({
|
|
263
|
+
id: 'form-1',
|
|
264
|
+
companyId: 'company-new'
|
|
265
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
266
|
+
companyId: 'company-from-user'
|
|
267
|
+
}));
|
|
268
|
+
expect(result.companyId).toBe('company-new');
|
|
269
|
+
});
|
|
270
|
+
it('preserves the existing companyId when the update DTO omits it', async ()=>{
|
|
271
|
+
await buildService(true);
|
|
272
|
+
const existing = buildForm({
|
|
273
|
+
id: 'form-1'
|
|
274
|
+
});
|
|
275
|
+
existing.companyId = 'company-original';
|
|
276
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
277
|
+
const result = await service.convertSingleDtoToEntity({
|
|
278
|
+
id: 'form-1',
|
|
279
|
+
name: 'Renamed'
|
|
280
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
281
|
+
companyId: 'company-from-user'
|
|
282
|
+
}));
|
|
283
|
+
expect(result.companyId).toBe('company-original');
|
|
284
|
+
});
|
|
285
|
+
it('falls back to the user companyId when the existing form has no companyId set', async ()=>{
|
|
286
|
+
await buildService(true);
|
|
287
|
+
// Simulate a stored Form (not FormWithCompany) — companyId is undefined on the entity.
|
|
288
|
+
const existing = buildForm({
|
|
289
|
+
id: 'form-1'
|
|
290
|
+
});
|
|
291
|
+
mockRepo.findOne.mockResolvedValue(existing);
|
|
292
|
+
const result = await service.convertSingleDtoToEntity({
|
|
293
|
+
id: 'form-1',
|
|
294
|
+
name: 'Renamed'
|
|
295
|
+
}, (0, _loggedusermock.buildMockUser)({
|
|
296
|
+
companyId: 'company-from-user'
|
|
297
|
+
}));
|
|
298
|
+
expect(result.companyId).toBe('company-from-user');
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
// ==========================================================================
|
|
303
|
+
// getSelectQuery
|
|
304
|
+
// ==========================================================================
|
|
305
|
+
describe('getSelectQuery', ()=>{
|
|
306
|
+
it('applies the default field list, prefixed with the entity name', async ()=>{
|
|
307
|
+
await buildService(false);
|
|
308
|
+
const query = {
|
|
309
|
+
select: jest.fn().mockReturnThis()
|
|
310
|
+
};
|
|
311
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), undefined);
|
|
312
|
+
expect(query.select).toHaveBeenCalledWith(expect.arrayContaining([
|
|
313
|
+
'form.id',
|
|
314
|
+
'form.name',
|
|
315
|
+
'form.schema',
|
|
316
|
+
'form.schemaVersion'
|
|
317
|
+
]));
|
|
318
|
+
});
|
|
319
|
+
it('appends form.companyId to the default fields when company feature is enabled', async ()=>{
|
|
320
|
+
await buildService(true);
|
|
321
|
+
const query = {
|
|
322
|
+
select: jest.fn().mockReturnThis()
|
|
323
|
+
};
|
|
324
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), undefined);
|
|
325
|
+
const selectedFields = query.select.mock.calls[0][0];
|
|
326
|
+
expect(selectedFields).toContain('form.companyId');
|
|
327
|
+
});
|
|
328
|
+
it('appends form.companyId even when a custom select list is provided', async ()=>{
|
|
329
|
+
await buildService(true);
|
|
330
|
+
const query = {
|
|
331
|
+
select: jest.fn().mockReturnThis()
|
|
332
|
+
};
|
|
333
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), [
|
|
334
|
+
'name'
|
|
335
|
+
]);
|
|
336
|
+
expect(query.select).toHaveBeenCalledWith([
|
|
337
|
+
'form.name',
|
|
338
|
+
'form.companyId'
|
|
339
|
+
]);
|
|
340
|
+
});
|
|
341
|
+
it('does not append companyId when company feature is disabled with a custom select', async ()=>{
|
|
342
|
+
await buildService(false);
|
|
343
|
+
const query = {
|
|
344
|
+
select: jest.fn().mockReturnThis()
|
|
345
|
+
};
|
|
346
|
+
await service.getSelectQuery(query, (0, _loggedusermock.buildMockUser)(), [
|
|
347
|
+
'name'
|
|
348
|
+
]);
|
|
349
|
+
expect(query.select).toHaveBeenCalledWith([
|
|
350
|
+
'form.name'
|
|
351
|
+
]);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
// ==========================================================================
|
|
355
|
+
// getFilterQuery — tag filtering across DB drivers
|
|
356
|
+
// ==========================================================================
|
|
357
|
+
describe('getFilterQuery', ()=>{
|
|
358
|
+
function buildQueryStub() {
|
|
359
|
+
return {
|
|
360
|
+
andWhere: jest.fn().mockReturnThis()
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
it('delegates directly to the base filter query when no tags filter is present', async ()=>{
|
|
364
|
+
await buildService(false);
|
|
365
|
+
const query = buildQueryStub();
|
|
366
|
+
await service.getFilterQuery(query, {
|
|
367
|
+
isActive: true
|
|
368
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
369
|
+
expect(query.andWhere).toHaveBeenCalledWith('form.isActive = :value', {
|
|
370
|
+
value: true
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
it('ignores an empty tags array (no tag filter applied)', async ()=>{
|
|
374
|
+
await buildService(false);
|
|
375
|
+
const query = buildQueryStub();
|
|
376
|
+
await service.getFilterQuery(query, {
|
|
377
|
+
tags: []
|
|
378
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
379
|
+
expect(query.andWhere).not.toHaveBeenCalled();
|
|
380
|
+
});
|
|
381
|
+
it('coerces a single non-array tag value into a one-item list', async ()=>{
|
|
382
|
+
await buildService(false);
|
|
383
|
+
jest.spyOn(_nestjscore.envConfig, 'getTypeOrmConfig').mockReturnValue({
|
|
384
|
+
type: 'mysql'
|
|
385
|
+
});
|
|
386
|
+
const query = buildQueryStub();
|
|
387
|
+
await service.getFilterQuery(query, {
|
|
388
|
+
tags: 'survey'
|
|
389
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
390
|
+
expect(query.andWhere).toHaveBeenCalledWith('JSON_CONTAINS(form.tags, :tagValues)', {
|
|
391
|
+
tagValues: JSON.stringify([
|
|
392
|
+
'survey'
|
|
393
|
+
])
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
it('uses jsonb containment for postgres', async ()=>{
|
|
397
|
+
await buildService(false);
|
|
398
|
+
jest.spyOn(_nestjscore.envConfig, 'getTypeOrmConfig').mockReturnValue({
|
|
399
|
+
type: 'postgres'
|
|
400
|
+
});
|
|
401
|
+
const query = buildQueryStub();
|
|
402
|
+
await service.getFilterQuery(query, {
|
|
403
|
+
tags: [
|
|
404
|
+
'survey',
|
|
405
|
+
'review'
|
|
406
|
+
]
|
|
407
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
408
|
+
expect(query.andWhere).toHaveBeenCalledWith('form.tags @> :tagValues::jsonb', {
|
|
409
|
+
tagValues: JSON.stringify([
|
|
410
|
+
'survey',
|
|
411
|
+
'review'
|
|
412
|
+
])
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
it('uses JSON_CONTAINS for mysql', async ()=>{
|
|
416
|
+
await buildService(false);
|
|
417
|
+
jest.spyOn(_nestjscore.envConfig, 'getTypeOrmConfig').mockReturnValue({
|
|
418
|
+
type: 'mysql'
|
|
419
|
+
});
|
|
420
|
+
const query = buildQueryStub();
|
|
421
|
+
await service.getFilterQuery(query, {
|
|
422
|
+
tags: [
|
|
423
|
+
'survey',
|
|
424
|
+
'review'
|
|
425
|
+
]
|
|
426
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
427
|
+
expect(query.andWhere).toHaveBeenCalledWith('JSON_CONTAINS(form.tags, :tagValues)', {
|
|
428
|
+
tagValues: JSON.stringify([
|
|
429
|
+
'survey',
|
|
430
|
+
'review'
|
|
431
|
+
])
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
it('falls back to per-tag LIKE matching for other drivers (e.g. sqlite)', async ()=>{
|
|
435
|
+
await buildService(false);
|
|
436
|
+
jest.spyOn(_nestjscore.envConfig, 'getTypeOrmConfig').mockReturnValue({
|
|
437
|
+
type: 'sqlite'
|
|
438
|
+
});
|
|
439
|
+
const query = buildQueryStub();
|
|
440
|
+
await service.getFilterQuery(query, {
|
|
441
|
+
tags: [
|
|
442
|
+
'survey',
|
|
443
|
+
'review'
|
|
444
|
+
]
|
|
445
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
446
|
+
expect(query.andWhere).toHaveBeenCalledWith('form.tags LIKE :tagValue0', {
|
|
447
|
+
tagValue0: '%"survey"%'
|
|
448
|
+
});
|
|
449
|
+
expect(query.andWhere).toHaveBeenCalledWith('form.tags LIKE :tagValue1', {
|
|
450
|
+
tagValue1: '%"review"%'
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
it('still applies remaining (non-tag) filters alongside the tag filter', async ()=>{
|
|
454
|
+
await buildService(false);
|
|
455
|
+
jest.spyOn(_nestjscore.envConfig, 'getTypeOrmConfig').mockReturnValue({
|
|
456
|
+
type: 'mysql'
|
|
457
|
+
});
|
|
458
|
+
const query = buildQueryStub();
|
|
459
|
+
await service.getFilterQuery(query, {
|
|
460
|
+
tags: [
|
|
461
|
+
'survey'
|
|
462
|
+
],
|
|
463
|
+
isActive: true
|
|
464
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
465
|
+
expect(query.andWhere).toHaveBeenCalledWith('form.isActive = :value', {
|
|
466
|
+
value: true
|
|
467
|
+
});
|
|
468
|
+
expect(query.andWhere).toHaveBeenCalledWith('JSON_CONTAINS(form.tags, :tagValues)', {
|
|
469
|
+
tagValues: JSON.stringify([
|
|
470
|
+
'survey'
|
|
471
|
+
])
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
// ==========================================================================
|
|
476
|
+
// getExtraManipulateQuery — company scoping
|
|
477
|
+
// ==========================================================================
|
|
478
|
+
describe('getExtraManipulateQuery', ()=>{
|
|
479
|
+
it('applies a company filter when company feature is enabled and user has a companyId', async ()=>{
|
|
480
|
+
await buildService(true);
|
|
481
|
+
const query = {
|
|
482
|
+
andWhere: jest.fn().mockReturnThis()
|
|
483
|
+
};
|
|
484
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)({
|
|
485
|
+
companyId: 'company-1'
|
|
486
|
+
}));
|
|
487
|
+
expect(query.andWhere).toHaveBeenCalledWith('form.companyId = :companyId', {
|
|
488
|
+
companyId: 'company-1'
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
it('does not filter by company when company feature is disabled', async ()=>{
|
|
492
|
+
await buildService(false);
|
|
493
|
+
const query = {
|
|
494
|
+
andWhere: jest.fn().mockReturnThis()
|
|
495
|
+
};
|
|
496
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)({
|
|
497
|
+
companyId: 'company-1'
|
|
498
|
+
}));
|
|
499
|
+
expect(query.andWhere).not.toHaveBeenCalled();
|
|
500
|
+
});
|
|
501
|
+
it('does not filter by company when the user has no companyId', async ()=>{
|
|
502
|
+
await buildService(true);
|
|
503
|
+
const query = {
|
|
504
|
+
andWhere: jest.fn().mockReturnThis()
|
|
505
|
+
};
|
|
506
|
+
await service.getExtraManipulateQuery(query, {}, (0, _loggedusermock.buildMockUser)({
|
|
507
|
+
companyId: undefined
|
|
508
|
+
}));
|
|
509
|
+
expect(query.andWhere).not.toHaveBeenCalled();
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
// ==========================================================================
|
|
513
|
+
// convertEntityToResponseDto
|
|
514
|
+
// ==========================================================================
|
|
515
|
+
describe('convertEntityToResponseDto', ()=>{
|
|
516
|
+
it('maps entity fields to the response shape and defaults companyId to null', async ()=>{
|
|
517
|
+
await buildService(false);
|
|
518
|
+
const entity = buildForm();
|
|
519
|
+
const dto = service.convertEntityToResponseDto(entity, false);
|
|
520
|
+
expect(dto).toMatchObject({
|
|
521
|
+
id: entity.id,
|
|
522
|
+
name: entity.name,
|
|
523
|
+
companyId: null
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
it('carries through companyId when present on the entity', async ()=>{
|
|
527
|
+
await buildService(true);
|
|
528
|
+
const entity = buildForm();
|
|
529
|
+
entity.companyId = 'company-1';
|
|
530
|
+
const dto = service.convertEntityToResponseDto(entity, false);
|
|
531
|
+
expect(dto.companyId).toBe('company-1');
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
// ==========================================================================
|
|
535
|
+
// Public form access
|
|
536
|
+
// ==========================================================================
|
|
537
|
+
describe('getPublicForm', ()=>{
|
|
538
|
+
it('returns the public projection of an active public form', async ()=>{
|
|
539
|
+
await buildService(false);
|
|
540
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
541
|
+
accessType: _formaccesstypeenum.FormAccessType.PUBLIC
|
|
542
|
+
}));
|
|
543
|
+
const result = await service.getPublicForm('form-uuid-1');
|
|
544
|
+
expect(result).toEqual({
|
|
545
|
+
id: 'form-uuid-1',
|
|
546
|
+
name: 'Customer Survey',
|
|
547
|
+
description: null,
|
|
548
|
+
schema: {
|
|
549
|
+
sections: [],
|
|
550
|
+
settings: {}
|
|
551
|
+
},
|
|
552
|
+
schemaVersion: 1
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
it('throws NotFoundException when the form is missing, inactive, or not public', async ()=>{
|
|
556
|
+
await buildService(false);
|
|
557
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
558
|
+
await expect(service.getPublicForm('missing')).rejects.toMatchObject({
|
|
559
|
+
response: expect.objectContaining({
|
|
560
|
+
messageKey: 'form.not.public'
|
|
561
|
+
})
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
describe('getFormForSubmission', ()=>{
|
|
566
|
+
it('throws NotFoundException when the form does not exist or is inactive', async ()=>{
|
|
567
|
+
await buildService(false);
|
|
568
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
569
|
+
await expect(service.getFormForSubmission('missing', null)).rejects.toMatchObject({
|
|
570
|
+
response: expect.objectContaining({
|
|
571
|
+
messageKey: 'form.not.found'
|
|
572
|
+
})
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
it('allows anonymous access to PUBLIC forms', async ()=>{
|
|
576
|
+
await buildService(false);
|
|
577
|
+
const form = buildForm({
|
|
578
|
+
accessType: _formaccesstypeenum.FormAccessType.PUBLIC
|
|
579
|
+
});
|
|
580
|
+
mockRepo.findOne.mockResolvedValue(form);
|
|
581
|
+
const result = await service.getFormForSubmission('form-uuid-1', null);
|
|
582
|
+
expect(result).toBe(form);
|
|
583
|
+
});
|
|
584
|
+
it('requires authentication for non-public forms', async ()=>{
|
|
585
|
+
await buildService(false);
|
|
586
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
587
|
+
accessType: _formaccesstypeenum.FormAccessType.AUTHENTICATED
|
|
588
|
+
}));
|
|
589
|
+
await expect(service.getFormForSubmission('form-uuid-1', null)).rejects.toMatchObject({
|
|
590
|
+
response: expect.objectContaining({
|
|
591
|
+
messageKey: 'form.auth.required'
|
|
592
|
+
})
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
it('allows an authenticated user to access an AUTHENTICATED form', async ()=>{
|
|
596
|
+
await buildService(false);
|
|
597
|
+
const form = buildForm({
|
|
598
|
+
accessType: _formaccesstypeenum.FormAccessType.AUTHENTICATED
|
|
599
|
+
});
|
|
600
|
+
mockRepo.findOne.mockResolvedValue(form);
|
|
601
|
+
const result = await service.getFormForSubmission('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
602
|
+
expect(result).toBe(form);
|
|
603
|
+
});
|
|
604
|
+
it('allows an authenticated user to access an ACTION_GROUP form (permission check deferred)', async ()=>{
|
|
605
|
+
await buildService(false);
|
|
606
|
+
const form = buildForm({
|
|
607
|
+
accessType: _formaccesstypeenum.FormAccessType.ACTION_GROUP,
|
|
608
|
+
actionGroups: [
|
|
609
|
+
'hr.submit'
|
|
610
|
+
]
|
|
611
|
+
});
|
|
612
|
+
mockRepo.findOne.mockResolvedValue(form);
|
|
613
|
+
const result = await service.getFormForSubmission('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
614
|
+
expect(result).toBe(form);
|
|
615
|
+
});
|
|
616
|
+
it('throws BadRequestException for an unrecognized access type', async ()=>{
|
|
617
|
+
await buildService(false);
|
|
618
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
619
|
+
accessType: 'weird'
|
|
620
|
+
}));
|
|
621
|
+
await expect(service.getFormForSubmission('form-uuid-1', (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
|
|
622
|
+
response: expect.objectContaining({
|
|
623
|
+
messageKey: 'form.invalid.access.type'
|
|
624
|
+
})
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
describe('getBySlug', ()=>{
|
|
629
|
+
it('returns the form response DTO when found', async ()=>{
|
|
630
|
+
await buildService(false);
|
|
631
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
632
|
+
slug: 'customer-survey'
|
|
633
|
+
}));
|
|
634
|
+
const result = await service.getBySlug('customer-survey');
|
|
635
|
+
expect(result?.slug).toBe('customer-survey');
|
|
636
|
+
});
|
|
637
|
+
it('returns null when the slug does not match any form', async ()=>{
|
|
638
|
+
await buildService(false);
|
|
639
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
640
|
+
const result = await service.getBySlug('missing-slug');
|
|
641
|
+
expect(result).toBeNull();
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
describe('getPublicFormBySlug', ()=>{
|
|
645
|
+
it('returns the public projection when found', async ()=>{
|
|
646
|
+
await buildService(false);
|
|
647
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
648
|
+
accessType: _formaccesstypeenum.FormAccessType.PUBLIC,
|
|
649
|
+
slug: 'public-slug'
|
|
650
|
+
}));
|
|
651
|
+
const result = await service.getPublicFormBySlug('public-slug');
|
|
652
|
+
expect(result).toEqual(expect.objectContaining({
|
|
653
|
+
id: 'form-uuid-1',
|
|
654
|
+
name: 'Customer Survey'
|
|
655
|
+
}));
|
|
656
|
+
});
|
|
657
|
+
it('returns null when no public active form matches the slug', async ()=>{
|
|
658
|
+
await buildService(false);
|
|
659
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
660
|
+
const result = await service.getPublicFormBySlug('missing-slug');
|
|
661
|
+
expect(result).toBeNull();
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
describe('getFormAccessInfo', ()=>{
|
|
665
|
+
it('returns access metadata for an existing form', async ()=>{
|
|
666
|
+
await buildService(false);
|
|
667
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
668
|
+
accessType: _formaccesstypeenum.FormAccessType.ACTION_GROUP,
|
|
669
|
+
actionGroups: [
|
|
670
|
+
'hr.submit'
|
|
671
|
+
]
|
|
672
|
+
}));
|
|
673
|
+
const result = await service.getFormAccessInfo('form-uuid-1');
|
|
674
|
+
expect(result).toEqual({
|
|
675
|
+
id: 'form-uuid-1',
|
|
676
|
+
name: 'Customer Survey',
|
|
677
|
+
description: null,
|
|
678
|
+
accessType: _formaccesstypeenum.FormAccessType.ACTION_GROUP,
|
|
679
|
+
actionGroups: [
|
|
680
|
+
'hr.submit'
|
|
681
|
+
],
|
|
682
|
+
isActive: true
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
it('throws NotFoundException when the form does not exist', async ()=>{
|
|
686
|
+
await buildService(false);
|
|
687
|
+
mockRepo.findOne.mockResolvedValue(null);
|
|
688
|
+
await expect(service.getFormAccessInfo('missing')).rejects.toMatchObject({
|
|
689
|
+
response: expect.objectContaining({
|
|
690
|
+
messageKey: 'form.not.found'
|
|
691
|
+
})
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
describe('getAuthenticatedForm', ()=>{
|
|
696
|
+
it('returns the public projection for an AUTHENTICATED form without checking permissions', async ()=>{
|
|
697
|
+
await buildService(false);
|
|
698
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
699
|
+
accessType: _formaccesstypeenum.FormAccessType.AUTHENTICATED
|
|
700
|
+
}));
|
|
701
|
+
const result = await service.getAuthenticatedForm('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
702
|
+
expect(result.id).toBe('form-uuid-1');
|
|
703
|
+
expect(_permissionutils.validateUserPermissions).not.toHaveBeenCalled();
|
|
704
|
+
});
|
|
705
|
+
it('returns the form when the user has one of the required action-group permissions', async ()=>{
|
|
706
|
+
await buildService(false);
|
|
707
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
708
|
+
accessType: _formaccesstypeenum.FormAccessType.ACTION_GROUP,
|
|
709
|
+
actionGroups: [
|
|
710
|
+
'hr.submit'
|
|
711
|
+
]
|
|
712
|
+
}));
|
|
713
|
+
_permissionutils.validateUserPermissions.mockResolvedValue(true);
|
|
714
|
+
const result = await service.getAuthenticatedForm('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
715
|
+
expect(result.id).toBe('form-uuid-1');
|
|
716
|
+
expect(_permissionutils.validateUserPermissions).toHaveBeenCalledWith(expect.any(Object), [
|
|
717
|
+
'hr.submit'
|
|
718
|
+
], expect.anything(), false);
|
|
719
|
+
});
|
|
720
|
+
it('throws ForbiddenException when the user lacks all action-group permissions', async ()=>{
|
|
721
|
+
await buildService(false);
|
|
722
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
723
|
+
accessType: _formaccesstypeenum.FormAccessType.ACTION_GROUP,
|
|
724
|
+
actionGroups: [
|
|
725
|
+
'hr.submit'
|
|
726
|
+
]
|
|
727
|
+
}));
|
|
728
|
+
_permissionutils.validateUserPermissions.mockResolvedValue(false);
|
|
729
|
+
await expect(service.getAuthenticatedForm('form-uuid-1', (0, _loggedusermock.buildMockUser)())).rejects.toMatchObject({
|
|
730
|
+
response: expect.objectContaining({
|
|
731
|
+
messageKey: 'form.access.denied'
|
|
732
|
+
})
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
it('skips the permission check when an ACTION_GROUP form has no configured action groups', async ()=>{
|
|
736
|
+
await buildService(false);
|
|
737
|
+
mockRepo.findOne.mockResolvedValue(buildForm({
|
|
738
|
+
accessType: _formaccesstypeenum.FormAccessType.ACTION_GROUP,
|
|
739
|
+
actionGroups: []
|
|
740
|
+
}));
|
|
741
|
+
const result = await service.getAuthenticatedForm('form-uuid-1', (0, _loggedusermock.buildMockUser)());
|
|
742
|
+
expect(result.id).toBe('form-uuid-1');
|
|
743
|
+
expect(_permissionutils.validateUserPermissions).not.toHaveBeenCalled();
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
});
|