@flusys/nestjs-form-builder 6.0.2 → 6.1.1
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,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _common = require("@nestjs/common");
|
|
6
|
+
const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
7
|
+
const _permissionutils = require("./permission.utils");
|
|
8
|
+
function buildMockCache() {
|
|
9
|
+
return {
|
|
10
|
+
get: jest.fn(),
|
|
11
|
+
set: jest.fn(),
|
|
12
|
+
del: jest.fn()
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
describe('validateUserPermissions', ()=>{
|
|
16
|
+
it('returns true immediately without touching the cache when no permissions are required', async ()=>{
|
|
17
|
+
const cache = buildMockCache();
|
|
18
|
+
const result = await (0, _permissionutils.validateUserPermissions)((0, _loggedusermock.buildMockUser)(), [], cache, false);
|
|
19
|
+
expect(result).toBe(true);
|
|
20
|
+
expect(cache.get).not.toHaveBeenCalled();
|
|
21
|
+
});
|
|
22
|
+
it('returns true when the user has at least one of the required permissions', async ()=>{
|
|
23
|
+
const cache = buildMockCache();
|
|
24
|
+
cache.get.mockResolvedValue([
|
|
25
|
+
'hr.survey.submit',
|
|
26
|
+
'other.permission'
|
|
27
|
+
]);
|
|
28
|
+
const result = await (0, _permissionutils.validateUserPermissions)((0, _loggedusermock.buildMockUser)(), [
|
|
29
|
+
'hr.survey.submit',
|
|
30
|
+
'employee.feedback.submit'
|
|
31
|
+
], cache, false);
|
|
32
|
+
expect(result).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
it('returns false when the user has none of the required permissions', async ()=>{
|
|
35
|
+
const cache = buildMockCache();
|
|
36
|
+
cache.get.mockResolvedValue([
|
|
37
|
+
'some.other.permission'
|
|
38
|
+
]);
|
|
39
|
+
const result = await (0, _permissionutils.validateUserPermissions)((0, _loggedusermock.buildMockUser)(), [
|
|
40
|
+
'hr.survey.submit'
|
|
41
|
+
], cache, false);
|
|
42
|
+
expect(result).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('treats a missing cache entry as no permissions (returns false)', async ()=>{
|
|
45
|
+
const cache = buildMockCache();
|
|
46
|
+
cache.get.mockResolvedValue(undefined);
|
|
47
|
+
const result = await (0, _permissionutils.validateUserPermissions)((0, _loggedusermock.buildMockUser)(), [
|
|
48
|
+
'hr.survey.submit'
|
|
49
|
+
], cache, false);
|
|
50
|
+
expect(result).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it('fails closed with a ForbiddenException when the cache lookup throws', async ()=>{
|
|
53
|
+
const cache = buildMockCache();
|
|
54
|
+
cache.get.mockRejectedValue(new Error('redis down'));
|
|
55
|
+
await expect((0, _permissionutils.validateUserPermissions)((0, _loggedusermock.buildMockUser)(), [
|
|
56
|
+
'hr.survey.submit'
|
|
57
|
+
], cache, false)).rejects.toMatchObject({
|
|
58
|
+
response: expect.objectContaining({
|
|
59
|
+
messageKey: 'form.permission.check.failed'
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
await expect((0, _permissionutils.validateUserPermissions)((0, _loggedusermock.buildMockUser)(), [
|
|
63
|
+
'hr.survey.submit'
|
|
64
|
+
], cache, false)).rejects.toBeInstanceOf(_common.ForbiddenException);
|
|
65
|
+
});
|
|
66
|
+
describe('cache key construction', ()=>{
|
|
67
|
+
it('uses a company/branch/user scoped key when the company feature is enabled and both are present', async ()=>{
|
|
68
|
+
const cache = buildMockCache();
|
|
69
|
+
cache.get.mockResolvedValue([]);
|
|
70
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
71
|
+
companyId: 'company-1',
|
|
72
|
+
branchId: 'branch-1',
|
|
73
|
+
id: 'user-1'
|
|
74
|
+
});
|
|
75
|
+
await (0, _permissionutils.validateUserPermissions)(user, [
|
|
76
|
+
'perm.read'
|
|
77
|
+
], cache, true);
|
|
78
|
+
expect(cache.get).toHaveBeenCalledWith('permissions:company:company-1:branch:branch-1:user:user-1');
|
|
79
|
+
});
|
|
80
|
+
it('uses "null" as the branch placeholder when branchId is missing under the company feature', async ()=>{
|
|
81
|
+
const cache = buildMockCache();
|
|
82
|
+
cache.get.mockResolvedValue([]);
|
|
83
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
84
|
+
companyId: 'company-1',
|
|
85
|
+
branchId: undefined,
|
|
86
|
+
id: 'user-1'
|
|
87
|
+
});
|
|
88
|
+
await (0, _permissionutils.validateUserPermissions)(user, [
|
|
89
|
+
'perm.read'
|
|
90
|
+
], cache, true);
|
|
91
|
+
expect(cache.get).toHaveBeenCalledWith('permissions:company:company-1:branch:null:user:user-1');
|
|
92
|
+
});
|
|
93
|
+
it('falls back to a plain user-scoped key when the company feature is enabled but companyId is missing', async ()=>{
|
|
94
|
+
const cache = buildMockCache();
|
|
95
|
+
cache.get.mockResolvedValue([]);
|
|
96
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
97
|
+
companyId: undefined,
|
|
98
|
+
id: 'user-1'
|
|
99
|
+
});
|
|
100
|
+
await (0, _permissionutils.validateUserPermissions)(user, [
|
|
101
|
+
'perm.read'
|
|
102
|
+
], cache, true);
|
|
103
|
+
expect(cache.get).toHaveBeenCalledWith('permissions:user:user-1');
|
|
104
|
+
});
|
|
105
|
+
it('uses a plain user-scoped key when the company feature is disabled', async ()=>{
|
|
106
|
+
const cache = buildMockCache();
|
|
107
|
+
cache.get.mockResolvedValue([]);
|
|
108
|
+
const user = (0, _loggedusermock.buildMockUser)({
|
|
109
|
+
companyId: 'company-1',
|
|
110
|
+
id: 'user-1'
|
|
111
|
+
});
|
|
112
|
+
await (0, _permissionutils.validateUserPermissions)(user, [
|
|
113
|
+
'perm.read'
|
|
114
|
+
], cache, false);
|
|
115
|
+
expect(cache.get).toHaveBeenCalledWith('permissions:user:user-1');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Test } from '@nestjs/testing';
|
|
2
|
+
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
3
|
+
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
4
|
+
import { FormResultController } from './form-result.controller';
|
|
5
|
+
import { FormResultService } from '../services/form-result.service';
|
|
6
|
+
function buildResultDto(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
id: 'result-1',
|
|
9
|
+
formId: 'form-1',
|
|
10
|
+
schemaVersionSnapshot: {
|
|
11
|
+
sections: []
|
|
12
|
+
},
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
data: {
|
|
15
|
+
field1: 'value1'
|
|
16
|
+
},
|
|
17
|
+
submittedById: 'user-uuid-1',
|
|
18
|
+
submittedAt: new Date(),
|
|
19
|
+
isDraft: false,
|
|
20
|
+
createdAt: new Date(),
|
|
21
|
+
updatedAt: new Date(),
|
|
22
|
+
deletedAt: null,
|
|
23
|
+
createdById: null,
|
|
24
|
+
updatedById: null,
|
|
25
|
+
deletedById: null,
|
|
26
|
+
...overrides
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
describe('FormResultController', ()=>{
|
|
30
|
+
let controller;
|
|
31
|
+
let mockService;
|
|
32
|
+
beforeEach(async ()=>{
|
|
33
|
+
mockService = {
|
|
34
|
+
insert: jest.fn(),
|
|
35
|
+
insertMany: jest.fn(),
|
|
36
|
+
findById: jest.fn(),
|
|
37
|
+
findByIds: jest.fn(),
|
|
38
|
+
getAll: jest.fn(),
|
|
39
|
+
update: jest.fn(),
|
|
40
|
+
updateMany: jest.fn(),
|
|
41
|
+
delete: jest.fn(),
|
|
42
|
+
submitForm: jest.fn(),
|
|
43
|
+
getMyDraft: jest.fn(),
|
|
44
|
+
updateDraft: jest.fn(),
|
|
45
|
+
getByFormId: jest.fn(),
|
|
46
|
+
hasUserSubmitted: jest.fn()
|
|
47
|
+
};
|
|
48
|
+
const module = await Test.createTestingModule({
|
|
49
|
+
controllers: [
|
|
50
|
+
FormResultController
|
|
51
|
+
],
|
|
52
|
+
// UtilsService is a real (dependency-free) provider — required to satisfy the
|
|
53
|
+
// Slug interceptor applied by the inherited insert/update endpoints.
|
|
54
|
+
providers: [
|
|
55
|
+
{
|
|
56
|
+
provide: FormResultService,
|
|
57
|
+
useValue: mockService
|
|
58
|
+
},
|
|
59
|
+
UtilsService
|
|
60
|
+
]
|
|
61
|
+
}).compile();
|
|
62
|
+
controller = module.get(FormResultController);
|
|
63
|
+
});
|
|
64
|
+
afterEach(()=>{
|
|
65
|
+
jest.clearAllMocks();
|
|
66
|
+
});
|
|
67
|
+
describe('submitForm', ()=>{
|
|
68
|
+
it('submits on behalf of the authenticated user (isPublic=false)', async ()=>{
|
|
69
|
+
const user = buildMockUser();
|
|
70
|
+
mockService.submitForm.mockResolvedValue(buildResultDto());
|
|
71
|
+
const result = await controller.submitForm({
|
|
72
|
+
formId: 'form-1',
|
|
73
|
+
data: {
|
|
74
|
+
field1: 'value1'
|
|
75
|
+
}
|
|
76
|
+
}, user);
|
|
77
|
+
expect(mockService.submitForm).toHaveBeenCalledWith({
|
|
78
|
+
formId: 'form-1',
|
|
79
|
+
data: {
|
|
80
|
+
field1: 'value1'
|
|
81
|
+
}
|
|
82
|
+
}, user, false);
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
success: true,
|
|
85
|
+
message: 'Form submitted successfully',
|
|
86
|
+
messageKey: 'form.result.submit.success',
|
|
87
|
+
data: expect.objectContaining({
|
|
88
|
+
id: 'result-1'
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
it('propagates errors from the service (e.g. permission denied)', async ()=>{
|
|
93
|
+
mockService.submitForm.mockRejectedValue(Object.assign(new Error('denied'), {
|
|
94
|
+
messageKey: 'form.access.denied'
|
|
95
|
+
}));
|
|
96
|
+
await expect(controller.submitForm({
|
|
97
|
+
formId: 'form-1',
|
|
98
|
+
data: {}
|
|
99
|
+
}, buildMockUser())).rejects.toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('submitPublicForm', ()=>{
|
|
103
|
+
it('submits without a user and marks the submission as public', async ()=>{
|
|
104
|
+
mockService.submitForm.mockResolvedValue(buildResultDto({
|
|
105
|
+
submittedById: null
|
|
106
|
+
}));
|
|
107
|
+
const result = await controller.submitPublicForm({
|
|
108
|
+
formId: 'form-1',
|
|
109
|
+
data: {}
|
|
110
|
+
});
|
|
111
|
+
expect(mockService.submitForm).toHaveBeenCalledWith({
|
|
112
|
+
formId: 'form-1',
|
|
113
|
+
data: {}
|
|
114
|
+
}, null, true);
|
|
115
|
+
expect(result.data.submittedById).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('getMyDraft', ()=>{
|
|
119
|
+
it('returns the current draft for the user', async ()=>{
|
|
120
|
+
const user = buildMockUser();
|
|
121
|
+
mockService.getMyDraft.mockResolvedValue(buildResultDto({
|
|
122
|
+
isDraft: true
|
|
123
|
+
}));
|
|
124
|
+
const result = await controller.getMyDraft({
|
|
125
|
+
formId: 'form-1'
|
|
126
|
+
}, user);
|
|
127
|
+
expect(mockService.getMyDraft).toHaveBeenCalledWith('form-1', user);
|
|
128
|
+
expect(result.data?.isDraft).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it('returns null data when no draft exists', async ()=>{
|
|
131
|
+
mockService.getMyDraft.mockResolvedValue(null);
|
|
132
|
+
const result = await controller.getMyDraft({
|
|
133
|
+
formId: 'form-1'
|
|
134
|
+
}, buildMockUser());
|
|
135
|
+
expect(result.data).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('updateDraft', ()=>{
|
|
139
|
+
it('updates the identified draft', async ()=>{
|
|
140
|
+
const user = buildMockUser();
|
|
141
|
+
mockService.updateDraft.mockResolvedValue(buildResultDto({
|
|
142
|
+
isDraft: true
|
|
143
|
+
}));
|
|
144
|
+
const result = await controller.updateDraft({
|
|
145
|
+
draftId: 'draft-1',
|
|
146
|
+
formId: 'form-1',
|
|
147
|
+
data: {
|
|
148
|
+
field1: 'new'
|
|
149
|
+
}
|
|
150
|
+
}, user);
|
|
151
|
+
expect(mockService.updateDraft).toHaveBeenCalledWith('draft-1', {
|
|
152
|
+
draftId: 'draft-1',
|
|
153
|
+
formId: 'form-1',
|
|
154
|
+
data: {
|
|
155
|
+
field1: 'new'
|
|
156
|
+
}
|
|
157
|
+
}, user);
|
|
158
|
+
expect(result.success).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe('getByFormId', ()=>{
|
|
162
|
+
it('returns paginated results with the requested page/pageSize', async ()=>{
|
|
163
|
+
mockService.getByFormId.mockResolvedValue({
|
|
164
|
+
data: [
|
|
165
|
+
buildResultDto()
|
|
166
|
+
],
|
|
167
|
+
total: 1
|
|
168
|
+
});
|
|
169
|
+
const result = await controller.getByFormId({
|
|
170
|
+
formId: 'form-1',
|
|
171
|
+
page: 1,
|
|
172
|
+
pageSize: 5
|
|
173
|
+
}, buildMockUser());
|
|
174
|
+
expect(mockService.getByFormId).toHaveBeenCalledWith('form-1', expect.any(Object), {
|
|
175
|
+
page: 1,
|
|
176
|
+
pageSize: 5
|
|
177
|
+
});
|
|
178
|
+
expect(result.meta).toEqual({
|
|
179
|
+
total: 1,
|
|
180
|
+
page: 1,
|
|
181
|
+
pageSize: 5,
|
|
182
|
+
count: 1
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
it('defaults to page 0 and pageSize 10 when not provided', async ()=>{
|
|
186
|
+
mockService.getByFormId.mockResolvedValue({
|
|
187
|
+
data: [],
|
|
188
|
+
total: 0
|
|
189
|
+
});
|
|
190
|
+
const result = await controller.getByFormId({
|
|
191
|
+
formId: 'form-1'
|
|
192
|
+
}, buildMockUser());
|
|
193
|
+
expect(mockService.getByFormId).toHaveBeenCalledWith('form-1', expect.any(Object), {
|
|
194
|
+
page: 0,
|
|
195
|
+
pageSize: 10
|
|
196
|
+
});
|
|
197
|
+
expect(result.meta).toEqual({
|
|
198
|
+
total: 0,
|
|
199
|
+
page: 0,
|
|
200
|
+
pageSize: 10,
|
|
201
|
+
count: 0
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('hasUserSubmitted', ()=>{
|
|
206
|
+
it('returns a truthy submitted message when the user has submitted', async ()=>{
|
|
207
|
+
mockService.hasUserSubmitted.mockResolvedValue(true);
|
|
208
|
+
const result = await controller.hasUserSubmitted({
|
|
209
|
+
formId: 'form-1'
|
|
210
|
+
}, buildMockUser());
|
|
211
|
+
expect(result).toEqual({
|
|
212
|
+
success: true,
|
|
213
|
+
message: 'User has submitted this form',
|
|
214
|
+
messageKey: 'form.result.has.submitted.success',
|
|
215
|
+
data: true
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
it('returns a not-submitted message when the user has not submitted', async ()=>{
|
|
219
|
+
mockService.hasUserSubmitted.mockResolvedValue(false);
|
|
220
|
+
const result = await controller.hasUserSubmitted({
|
|
221
|
+
formId: 'form-1'
|
|
222
|
+
}, buildMockUser());
|
|
223
|
+
expect(result).toEqual({
|
|
224
|
+
success: true,
|
|
225
|
+
message: 'User has not submitted this form',
|
|
226
|
+
messageKey: 'form.result.has.not.submitted.success',
|
|
227
|
+
data: false
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
// ==========================================================================
|
|
232
|
+
// Inherited base CRUD wiring (createApiController)
|
|
233
|
+
// ==========================================================================
|
|
234
|
+
describe('inherited CRUD endpoints', ()=>{
|
|
235
|
+
it('delete delegates to FormResultService.delete', async ()=>{
|
|
236
|
+
mockService.delete.mockResolvedValue(null);
|
|
237
|
+
const user = buildMockUser();
|
|
238
|
+
const result = await controller.delete({
|
|
239
|
+
id: [
|
|
240
|
+
'result-1'
|
|
241
|
+
],
|
|
242
|
+
type: 'delete'
|
|
243
|
+
}, user);
|
|
244
|
+
expect(mockService.delete).toHaveBeenCalledWith({
|
|
245
|
+
id: [
|
|
246
|
+
'result-1'
|
|
247
|
+
],
|
|
248
|
+
type: 'delete'
|
|
249
|
+
}, user);
|
|
250
|
+
expect(result.success).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { Test } from '@nestjs/testing';
|
|
2
|
+
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
3
|
+
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
4
|
+
import { FormAccessType } from '../enums/form-access-type.enum';
|
|
5
|
+
import { FormController } from './form.controller';
|
|
6
|
+
import { FormService } from '../services/form.service';
|
|
7
|
+
describe('FormController', ()=>{
|
|
8
|
+
let controller;
|
|
9
|
+
let mockService;
|
|
10
|
+
beforeEach(async ()=>{
|
|
11
|
+
mockService = {
|
|
12
|
+
insert: jest.fn(),
|
|
13
|
+
insertMany: jest.fn(),
|
|
14
|
+
findById: jest.fn(),
|
|
15
|
+
findByIds: jest.fn(),
|
|
16
|
+
getAll: jest.fn(),
|
|
17
|
+
update: jest.fn(),
|
|
18
|
+
updateMany: jest.fn(),
|
|
19
|
+
delete: jest.fn(),
|
|
20
|
+
getFormAccessInfo: jest.fn(),
|
|
21
|
+
getPublicForm: jest.fn(),
|
|
22
|
+
getAuthenticatedForm: jest.fn(),
|
|
23
|
+
getBySlug: jest.fn(),
|
|
24
|
+
getPublicFormBySlug: jest.fn()
|
|
25
|
+
};
|
|
26
|
+
const module = await Test.createTestingModule({
|
|
27
|
+
controllers: [
|
|
28
|
+
FormController
|
|
29
|
+
],
|
|
30
|
+
// UtilsService is a real (dependency-free) provider — required to satisfy the
|
|
31
|
+
// Slug interceptor applied by the inherited insert/update endpoints.
|
|
32
|
+
providers: [
|
|
33
|
+
{
|
|
34
|
+
provide: FormService,
|
|
35
|
+
useValue: mockService
|
|
36
|
+
},
|
|
37
|
+
UtilsService
|
|
38
|
+
]
|
|
39
|
+
}).compile();
|
|
40
|
+
controller = module.get(FormController);
|
|
41
|
+
});
|
|
42
|
+
afterEach(()=>{
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
describe('getFormAccessInfo', ()=>{
|
|
46
|
+
it('returns access info without requiring authentication', async ()=>{
|
|
47
|
+
mockService.getFormAccessInfo.mockResolvedValue({
|
|
48
|
+
id: 'form-1',
|
|
49
|
+
name: 'Survey',
|
|
50
|
+
description: null,
|
|
51
|
+
accessType: FormAccessType.PUBLIC,
|
|
52
|
+
actionGroups: null,
|
|
53
|
+
isActive: true
|
|
54
|
+
});
|
|
55
|
+
const result = await controller.getFormAccessInfo('form-1');
|
|
56
|
+
expect(result).toEqual({
|
|
57
|
+
success: true,
|
|
58
|
+
message: 'Form access info retrieved',
|
|
59
|
+
messageKey: 'form.get.access.info.success',
|
|
60
|
+
data: expect.objectContaining({
|
|
61
|
+
id: 'form-1',
|
|
62
|
+
accessType: FormAccessType.PUBLIC
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
expect(mockService.getFormAccessInfo).toHaveBeenCalledWith('form-1');
|
|
66
|
+
});
|
|
67
|
+
it('propagates NotFoundException raised by the service', async ()=>{
|
|
68
|
+
mockService.getFormAccessInfo.mockRejectedValue(Object.assign(new Error('not found'), {
|
|
69
|
+
messageKey: 'form.not.found'
|
|
70
|
+
}));
|
|
71
|
+
await expect(controller.getFormAccessInfo('missing')).rejects.toThrow();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('getPublicForm', ()=>{
|
|
75
|
+
it('returns the public form projection', async ()=>{
|
|
76
|
+
mockService.getPublicForm.mockResolvedValue({
|
|
77
|
+
id: 'form-1',
|
|
78
|
+
name: 'Survey',
|
|
79
|
+
description: null,
|
|
80
|
+
schema: {
|
|
81
|
+
sections: []
|
|
82
|
+
},
|
|
83
|
+
schemaVersion: 1
|
|
84
|
+
});
|
|
85
|
+
const result = await controller.getPublicForm('form-1');
|
|
86
|
+
expect(result.success).toBe(true);
|
|
87
|
+
expect(result.messageKey).toBe('form.get.success');
|
|
88
|
+
expect(result.data.id).toBe('form-1');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('getAuthenticatedForm', ()=>{
|
|
92
|
+
it('forwards the current user to the service and returns the form', async ()=>{
|
|
93
|
+
const user = buildMockUser();
|
|
94
|
+
mockService.getAuthenticatedForm.mockResolvedValue({
|
|
95
|
+
id: 'form-1',
|
|
96
|
+
name: 'Survey',
|
|
97
|
+
description: null,
|
|
98
|
+
schema: {},
|
|
99
|
+
schemaVersion: 1
|
|
100
|
+
});
|
|
101
|
+
const result = await controller.getAuthenticatedForm('form-1', user);
|
|
102
|
+
expect(mockService.getAuthenticatedForm).toHaveBeenCalledWith('form-1', user);
|
|
103
|
+
expect(result.data.id).toBe('form-1');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('getBySlug', ()=>{
|
|
107
|
+
it('returns the form when found by slug', async ()=>{
|
|
108
|
+
mockService.getBySlug.mockResolvedValue({
|
|
109
|
+
id: 'form-1',
|
|
110
|
+
name: 'Survey',
|
|
111
|
+
description: null,
|
|
112
|
+
slug: 'survey',
|
|
113
|
+
schema: {},
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
accessType: FormAccessType.AUTHENTICATED,
|
|
116
|
+
actionGroups: null,
|
|
117
|
+
tags: null,
|
|
118
|
+
companyId: null,
|
|
119
|
+
isActive: true,
|
|
120
|
+
createdAt: new Date(),
|
|
121
|
+
updatedAt: new Date(),
|
|
122
|
+
deletedAt: null,
|
|
123
|
+
createdById: null,
|
|
124
|
+
updatedById: null,
|
|
125
|
+
deletedById: null
|
|
126
|
+
});
|
|
127
|
+
const result = await controller.getBySlug('survey', buildMockUser());
|
|
128
|
+
expect(result.data?.slug).toBe('survey');
|
|
129
|
+
});
|
|
130
|
+
it('returns null data when no form matches the slug', async ()=>{
|
|
131
|
+
mockService.getBySlug.mockResolvedValue(null);
|
|
132
|
+
const result = await controller.getBySlug('missing', buildMockUser());
|
|
133
|
+
expect(result.data).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('getPublicFormBySlug', ()=>{
|
|
137
|
+
it('returns the public form projection when found', async ()=>{
|
|
138
|
+
mockService.getPublicFormBySlug.mockResolvedValue({
|
|
139
|
+
id: 'form-1',
|
|
140
|
+
name: 'Survey',
|
|
141
|
+
description: null,
|
|
142
|
+
schema: {},
|
|
143
|
+
schemaVersion: 1
|
|
144
|
+
});
|
|
145
|
+
const result = await controller.getPublicFormBySlug('survey');
|
|
146
|
+
expect(result.data?.id).toBe('form-1');
|
|
147
|
+
});
|
|
148
|
+
it('returns null data when no public form matches the slug', async ()=>{
|
|
149
|
+
mockService.getPublicFormBySlug.mockResolvedValue(null);
|
|
150
|
+
const result = await controller.getPublicFormBySlug('missing');
|
|
151
|
+
expect(result.data).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
// ==========================================================================
|
|
155
|
+
// Inherited base CRUD wiring (createApiController)
|
|
156
|
+
// ==========================================================================
|
|
157
|
+
describe('inherited CRUD endpoints', ()=>{
|
|
158
|
+
it('insert delegates to FormService.insert and wraps the result', async ()=>{
|
|
159
|
+
mockService.insert.mockResolvedValue({
|
|
160
|
+
id: 'form-1',
|
|
161
|
+
name: 'Survey',
|
|
162
|
+
description: null,
|
|
163
|
+
slug: null,
|
|
164
|
+
schema: {},
|
|
165
|
+
schemaVersion: 1,
|
|
166
|
+
accessType: FormAccessType.AUTHENTICATED,
|
|
167
|
+
actionGroups: null,
|
|
168
|
+
tags: null,
|
|
169
|
+
companyId: null,
|
|
170
|
+
isActive: true,
|
|
171
|
+
createdAt: new Date(),
|
|
172
|
+
updatedAt: new Date(),
|
|
173
|
+
deletedAt: null,
|
|
174
|
+
createdById: null,
|
|
175
|
+
updatedById: null,
|
|
176
|
+
deletedById: null
|
|
177
|
+
});
|
|
178
|
+
const user = buildMockUser();
|
|
179
|
+
const result = await controller.insert({
|
|
180
|
+
name: 'Survey',
|
|
181
|
+
schema: {}
|
|
182
|
+
}, user);
|
|
183
|
+
expect(mockService.insert).toHaveBeenCalledWith({
|
|
184
|
+
name: 'Survey',
|
|
185
|
+
schema: {}
|
|
186
|
+
}, user);
|
|
187
|
+
expect(result.success).toBe(true);
|
|
188
|
+
expect(result.data.name).toBe('Survey');
|
|
189
|
+
});
|
|
190
|
+
it('delete delegates to FormService.delete', async ()=>{
|
|
191
|
+
mockService.delete.mockResolvedValue(null);
|
|
192
|
+
const user = buildMockUser();
|
|
193
|
+
const result = await controller.delete({
|
|
194
|
+
id: [
|
|
195
|
+
'form-1'
|
|
196
|
+
],
|
|
197
|
+
type: 'delete'
|
|
198
|
+
}, user);
|
|
199
|
+
expect(mockService.delete).toHaveBeenCalledWith({
|
|
200
|
+
id: [
|
|
201
|
+
'form-1'
|
|
202
|
+
],
|
|
203
|
+
type: 'delete'
|
|
204
|
+
}, user);
|
|
205
|
+
expect(result.success).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { formBuilderSwaggerConfig } from './form-builder-swagger.config';
|
|
2
|
+
describe('formBuilderSwaggerConfig', ()=>{
|
|
3
|
+
it('should default to company feature enabled and single-database mode when no config is given', ()=>{
|
|
4
|
+
const config = formBuilderSwaggerConfig();
|
|
5
|
+
expect(config.title).toBe('Form Builder API');
|
|
6
|
+
expect(config.path).toBe('api/docs/form-builder');
|
|
7
|
+
expect(config.bearerAuth).toBe(true);
|
|
8
|
+
expect(config.excludeTags).toEqual([]);
|
|
9
|
+
expect(config.excludeSchemaProperties).toBeUndefined();
|
|
10
|
+
expect(config.description).not.toContain('Multi-Tenant Mode');
|
|
11
|
+
});
|
|
12
|
+
it('should exclude companyId schema properties when the company feature is disabled', ()=>{
|
|
13
|
+
const config = formBuilderSwaggerConfig({
|
|
14
|
+
enableCompanyFeature: false
|
|
15
|
+
});
|
|
16
|
+
expect(config.excludeSchemaProperties).toEqual([
|
|
17
|
+
{
|
|
18
|
+
schemaName: 'CreateFormDto',
|
|
19
|
+
properties: [
|
|
20
|
+
'companyId'
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
schemaName: 'UpdateFormDto',
|
|
25
|
+
properties: [
|
|
26
|
+
'companyId'
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
schemaName: 'FormQueryDto',
|
|
31
|
+
properties: [
|
|
32
|
+
'companyId'
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
schemaName: 'FormResponseDto',
|
|
37
|
+
properties: [
|
|
38
|
+
'companyId'
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
it('should not exclude any schema properties when the company feature is enabled', ()=>{
|
|
44
|
+
const config = formBuilderSwaggerConfig({
|
|
45
|
+
enableCompanyFeature: true
|
|
46
|
+
});
|
|
47
|
+
expect(config.excludeSchemaProperties).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
it('should describe company isolation and multi-tenant support only when the company feature is enabled', ()=>{
|
|
50
|
+
const withCompany = formBuilderSwaggerConfig({
|
|
51
|
+
enableCompanyFeature: true
|
|
52
|
+
});
|
|
53
|
+
const withoutCompany = formBuilderSwaggerConfig({
|
|
54
|
+
enableCompanyFeature: false
|
|
55
|
+
});
|
|
56
|
+
expect(withCompany.description).toContain('Company isolation');
|
|
57
|
+
expect(withCompany.description).toContain('## Multi-Tenant Support');
|
|
58
|
+
expect(withoutCompany.description).not.toContain('Company isolation');
|
|
59
|
+
expect(withoutCompany.description).not.toContain('## Multi-Tenant Support');
|
|
60
|
+
});
|
|
61
|
+
it('should mention multi-tenant mode in the description only for multi-tenant database mode', ()=>{
|
|
62
|
+
const multiTenant = formBuilderSwaggerConfig({
|
|
63
|
+
databaseMode: 'multi-tenant'
|
|
64
|
+
});
|
|
65
|
+
const single = formBuilderSwaggerConfig({
|
|
66
|
+
databaseMode: 'single'
|
|
67
|
+
});
|
|
68
|
+
expect(multiTenant.description).toContain('Multi-Tenant Mode');
|
|
69
|
+
expect(single.description).not.toContain('Multi-Tenant Mode');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getFormBuilderEntitiesByConfig, FormCoreEntities, FormCompanyEntities } from './index';
|
|
2
|
+
import { FormResult } from './form-result.entity';
|
|
3
|
+
describe('getFormBuilderEntitiesByConfig', ()=>{
|
|
4
|
+
it('returns the core entities when the company feature is disabled', ()=>{
|
|
5
|
+
expect(getFormBuilderEntitiesByConfig(false)).toBe(FormCoreEntities);
|
|
6
|
+
});
|
|
7
|
+
it('returns the company-scoped entities when the company feature is enabled', ()=>{
|
|
8
|
+
expect(getFormBuilderEntitiesByConfig(true)).toBe(FormCompanyEntities);
|
|
9
|
+
});
|
|
10
|
+
it('FormResult is shared between core and company entity sets (not company-scoped)', ()=>{
|
|
11
|
+
expect(getFormBuilderEntitiesByConfig(false)).toContain(FormResult);
|
|
12
|
+
expect(getFormBuilderEntitiesByConfig(true)).toContain(FormResult);
|
|
13
|
+
});
|
|
14
|
+
});
|