@flusys/nestjs-auth 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/authentication.controller.spec.js +219 -0
- package/cjs/controllers/branch.controller.spec.js +134 -0
- package/cjs/controllers/company-selection.controller.spec.js +84 -0
- package/cjs/controllers/company.controller.spec.js +131 -0
- package/cjs/controllers/email-verification.controller.spec.js +142 -0
- package/cjs/controllers/user-permission.controller.spec.js +138 -0
- package/cjs/controllers/user.controller.spec.js +204 -0
- package/cjs/docs/auth-swagger.config.spec.js +130 -0
- package/cjs/entities/index.spec.js +73 -0
- package/cjs/helpers/auth-response.mapper.js +46 -0
- package/cjs/helpers/auth-response.mapper.spec.js +139 -0
- package/cjs/helpers/index.js +19 -0
- package/cjs/helpers/token.helper.js +58 -0
- package/cjs/helpers/token.helper.spec.js +86 -0
- package/cjs/index.js +1 -0
- package/cjs/interceptors/clear-token.interceptor.spec.js +107 -0
- package/cjs/interceptors/set-token.interceptor.spec.js +165 -0
- package/cjs/modules/auth.module.spec.js +243 -0
- package/cjs/services/auth-config.service.spec.js +97 -0
- package/cjs/services/auth-datasource.provider.spec.js +161 -0
- package/cjs/services/auth-email.service.spec.js +404 -0
- package/cjs/services/authentication.service.js +37 -109
- package/cjs/services/authentication.service.spec.js +904 -0
- package/cjs/services/branch.service.spec.js +154 -0
- package/cjs/services/company-selection-session.service.spec.js +90 -0
- package/cjs/services/company.service.spec.js +113 -0
- package/cjs/services/user-permission.service.spec.js +202 -0
- package/cjs/services/user.service.spec.js +704 -0
- package/cjs/strategies/jwt.strategy.js +1 -1
- package/cjs/strategies/jwt.strategy.spec.js +166 -0
- package/cjs/validators/password.validator.spec.js +161 -0
- package/entities/pending-registration.entity.d.ts +2 -7
- package/fesm/controllers/authentication.controller.spec.js +215 -0
- package/fesm/controllers/branch.controller.spec.js +130 -0
- package/fesm/controllers/company-selection.controller.spec.js +80 -0
- package/fesm/controllers/company.controller.spec.js +127 -0
- package/fesm/controllers/email-verification.controller.spec.js +138 -0
- package/fesm/controllers/user-permission.controller.spec.js +134 -0
- package/fesm/controllers/user.controller.spec.js +200 -0
- package/fesm/docs/auth-swagger.config.spec.js +126 -0
- package/fesm/entities/index.spec.js +69 -0
- package/fesm/helpers/auth-response.mapper.js +25 -0
- package/fesm/helpers/auth-response.mapper.spec.js +135 -0
- package/fesm/helpers/index.js +2 -0
- package/fesm/helpers/token.helper.js +40 -0
- package/fesm/helpers/token.helper.spec.js +82 -0
- package/fesm/index.js +1 -0
- package/fesm/interceptors/clear-token.interceptor.spec.js +103 -0
- package/fesm/interceptors/set-token.interceptor.spec.js +161 -0
- package/fesm/interfaces/authentication.interface.js +1 -1
- package/fesm/modules/auth.module.spec.js +239 -0
- package/fesm/services/auth-config.service.spec.js +93 -0
- package/fesm/services/auth-datasource.provider.spec.js +157 -0
- package/fesm/services/auth-email.service.spec.js +359 -0
- package/fesm/services/authentication.service.js +37 -109
- package/fesm/services/authentication.service.spec.js +859 -0
- package/fesm/services/branch.service.spec.js +150 -0
- package/fesm/services/company-selection-session.service.spec.js +86 -0
- package/fesm/services/company.service.spec.js +109 -0
- package/fesm/services/user-permission.service.spec.js +198 -0
- package/fesm/services/user.service.spec.js +659 -0
- package/fesm/strategies/jwt.strategy.js +1 -1
- package/fesm/strategies/jwt.strategy.spec.js +162 -0
- package/fesm/validators/password.validator.spec.js +157 -0
- package/helpers/auth-response.mapper.d.ts +22 -0
- package/helpers/index.d.ts +2 -0
- package/helpers/token.helper.d.ts +8 -0
- package/index.d.ts +1 -0
- package/interfaces/authentication.interface.d.ts +7 -0
- package/package.json +8 -3
- package/services/authentication.service.d.ts +1 -8
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
2
|
+
import { UserController } from './user.controller';
|
|
3
|
+
function buildUser(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
id: 'user-1',
|
|
6
|
+
name: 'Test User',
|
|
7
|
+
email: 'test@example.com',
|
|
8
|
+
phone: null,
|
|
9
|
+
isActive: true,
|
|
10
|
+
readOnly: false,
|
|
11
|
+
createdAt: new Date(),
|
|
12
|
+
updatedAt: new Date(),
|
|
13
|
+
...overrides
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe('UserController', ()=>{
|
|
17
|
+
let controller;
|
|
18
|
+
let mockUserService;
|
|
19
|
+
beforeEach(()=>{
|
|
20
|
+
mockUserService = {
|
|
21
|
+
insert: jest.fn(),
|
|
22
|
+
insertMany: jest.fn(),
|
|
23
|
+
findById: jest.fn(),
|
|
24
|
+
findByIds: jest.fn(),
|
|
25
|
+
update: jest.fn(),
|
|
26
|
+
updateMany: jest.fn(),
|
|
27
|
+
bulkUpsert: jest.fn(),
|
|
28
|
+
getAll: jest.fn(),
|
|
29
|
+
delete: jest.fn(),
|
|
30
|
+
getProfileWithExtras: jest.fn(),
|
|
31
|
+
verifyEmail: jest.fn(),
|
|
32
|
+
verifyPhone: jest.fn(),
|
|
33
|
+
updateStatus: jest.fn(),
|
|
34
|
+
profile: jest.fn(),
|
|
35
|
+
getProfileSections: jest.fn(),
|
|
36
|
+
getProfileSectionData: jest.fn(),
|
|
37
|
+
updateProfileSection: jest.fn(),
|
|
38
|
+
getProfileCompletion: jest.fn()
|
|
39
|
+
};
|
|
40
|
+
controller = new UserController(mockUserService);
|
|
41
|
+
});
|
|
42
|
+
describe('insert', ()=>{
|
|
43
|
+
it('creates a user and returns the mapped response', async ()=>{
|
|
44
|
+
const user = buildMockUser();
|
|
45
|
+
mockUserService.insert.mockResolvedValue(buildUser());
|
|
46
|
+
const result = await controller.insert({
|
|
47
|
+
name: 'Test User',
|
|
48
|
+
email: 'test@example.com',
|
|
49
|
+
password: 'Password1!'
|
|
50
|
+
}, user);
|
|
51
|
+
expect(mockUserService.insert).toHaveBeenCalledWith({
|
|
52
|
+
name: 'Test User',
|
|
53
|
+
email: 'test@example.com',
|
|
54
|
+
password: 'Password1!'
|
|
55
|
+
}, user);
|
|
56
|
+
expect(result.success).toBe(true);
|
|
57
|
+
expect(result.data).toEqual(expect.objectContaining({
|
|
58
|
+
id: 'user-1'
|
|
59
|
+
}));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('getAll', ()=>{
|
|
63
|
+
it('paginates users and maps the response list', async ()=>{
|
|
64
|
+
mockUserService.getAll.mockResolvedValue({
|
|
65
|
+
data: [
|
|
66
|
+
buildUser()
|
|
67
|
+
],
|
|
68
|
+
total: 1
|
|
69
|
+
});
|
|
70
|
+
const result = await controller.getAll({}, buildMockUser(), '');
|
|
71
|
+
expect(mockUserService.getAll).toHaveBeenCalledWith('', {}, buildMockUser());
|
|
72
|
+
expect(result.data).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('getProfileWithExtras', ()=>{
|
|
76
|
+
it('returns the enriched profile payload', async ()=>{
|
|
77
|
+
const user = buildMockUser();
|
|
78
|
+
mockUserService.getProfileWithExtras.mockResolvedValue({
|
|
79
|
+
...buildUser(),
|
|
80
|
+
completionPercentage: 80
|
|
81
|
+
});
|
|
82
|
+
const result = await controller.getProfileWithExtras('user-1', user);
|
|
83
|
+
expect(mockUserService.getProfileWithExtras).toHaveBeenCalledWith('user-1', user);
|
|
84
|
+
expect(result).toEqual({
|
|
85
|
+
success: true,
|
|
86
|
+
message: 'Profile extras retrieved successfully',
|
|
87
|
+
messageKey: expect.any(String),
|
|
88
|
+
data: expect.objectContaining({
|
|
89
|
+
completionPercentage: 80
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('verifyEmail', ()=>{
|
|
95
|
+
it('verifies the email and returns a success message', async ()=>{
|
|
96
|
+
mockUserService.verifyEmail.mockResolvedValue(undefined);
|
|
97
|
+
const result = await controller.verifyEmail('user-1');
|
|
98
|
+
expect(mockUserService.verifyEmail).toHaveBeenCalledWith('user-1');
|
|
99
|
+
expect(result.success).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('verifyPhone', ()=>{
|
|
103
|
+
it('verifies the phone and returns a success message', async ()=>{
|
|
104
|
+
mockUserService.verifyPhone.mockResolvedValue(undefined);
|
|
105
|
+
const result = await controller.verifyPhone('user-1');
|
|
106
|
+
expect(mockUserService.verifyPhone).toHaveBeenCalledWith('user-1');
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('updateStatus', ()=>{
|
|
111
|
+
it('updates the active status for the given user', async ()=>{
|
|
112
|
+
const user = buildMockUser();
|
|
113
|
+
mockUserService.updateStatus.mockResolvedValue(undefined);
|
|
114
|
+
const result = await controller.updateStatus('user-1', false, user);
|
|
115
|
+
expect(mockUserService.updateStatus).toHaveBeenCalledWith('user-1', false, user);
|
|
116
|
+
expect(result.success).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('profile', ()=>{
|
|
120
|
+
it('updates the current user profile and returns the data', async ()=>{
|
|
121
|
+
const user = buildMockUser();
|
|
122
|
+
mockUserService.profile.mockResolvedValue('user-1');
|
|
123
|
+
const result = await controller.profile({
|
|
124
|
+
id: 'user-1',
|
|
125
|
+
name: 'New Name'
|
|
126
|
+
}, user);
|
|
127
|
+
expect(mockUserService.profile).toHaveBeenCalledWith({
|
|
128
|
+
id: 'user-1',
|
|
129
|
+
name: 'New Name'
|
|
130
|
+
}, user);
|
|
131
|
+
expect(result).toEqual({
|
|
132
|
+
success: true,
|
|
133
|
+
message: 'Profile updated successfully',
|
|
134
|
+
messageKey: expect.any(String),
|
|
135
|
+
data: 'user-1'
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('getProfileSections', ()=>{
|
|
140
|
+
it('returns the profile section definitions', async ()=>{
|
|
141
|
+
mockUserService.getProfileSections.mockResolvedValue([
|
|
142
|
+
{
|
|
143
|
+
id: 's1',
|
|
144
|
+
name: 'Basic',
|
|
145
|
+
order: 1
|
|
146
|
+
}
|
|
147
|
+
]);
|
|
148
|
+
const result = await controller.getProfileSections();
|
|
149
|
+
expect(result.data).toEqual([
|
|
150
|
+
{
|
|
151
|
+
id: 's1',
|
|
152
|
+
name: 'Basic',
|
|
153
|
+
order: 1
|
|
154
|
+
}
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('getProfileSectionData', ()=>{
|
|
159
|
+
it('returns data for the requested section', async ()=>{
|
|
160
|
+
const user = buildMockUser();
|
|
161
|
+
mockUserService.getProfileSectionData.mockResolvedValue({
|
|
162
|
+
sectionId: 's1',
|
|
163
|
+
data: {
|
|
164
|
+
foo: 'bar'
|
|
165
|
+
},
|
|
166
|
+
isComplete: true
|
|
167
|
+
});
|
|
168
|
+
const result = await controller.getProfileSectionData('user-1', 's1', user);
|
|
169
|
+
expect(mockUserService.getProfileSectionData).toHaveBeenCalledWith('user-1', 's1', user);
|
|
170
|
+
expect(result.data).toEqual(expect.objectContaining({
|
|
171
|
+
sectionId: 's1',
|
|
172
|
+
isComplete: true
|
|
173
|
+
}));
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe('updateProfileSection', ()=>{
|
|
177
|
+
it('updates the requested section', async ()=>{
|
|
178
|
+
const user = buildMockUser();
|
|
179
|
+
mockUserService.updateProfileSection.mockResolvedValue(undefined);
|
|
180
|
+
const result = await controller.updateProfileSection('user-1', 's1', {
|
|
181
|
+
foo: 'bar'
|
|
182
|
+
}, user);
|
|
183
|
+
expect(mockUserService.updateProfileSection).toHaveBeenCalledWith('user-1', 's1', {
|
|
184
|
+
foo: 'bar'
|
|
185
|
+
}, user);
|
|
186
|
+
expect(result.success).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
describe('getProfileCompletion', ()=>{
|
|
190
|
+
it('returns the completion percentage', async ()=>{
|
|
191
|
+
const user = buildMockUser();
|
|
192
|
+
mockUserService.getProfileCompletion.mockResolvedValue(42);
|
|
193
|
+
const result = await controller.getProfileCompletion('user-1', user);
|
|
194
|
+
expect(mockUserService.getProfileCompletion).toHaveBeenCalledWith('user-1', user);
|
|
195
|
+
expect(result.data).toEqual({
|
|
196
|
+
completionPercentage: 42
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { authSwaggerConfig } from './auth-swagger.config';
|
|
2
|
+
describe('authSwaggerConfig', ()=>{
|
|
3
|
+
it('should default to both features enabled and single-database mode when no config is given', ()=>{
|
|
4
|
+
const config = authSwaggerConfig();
|
|
5
|
+
expect(config.title).toBe('Auth API');
|
|
6
|
+
expect(config.path).toBe('api/docs/auth');
|
|
7
|
+
expect(config.bearerAuth).toBe(true);
|
|
8
|
+
expect(config.excludeTags).toEqual([]);
|
|
9
|
+
expect(config.excludeSchemaProperties).toBeUndefined();
|
|
10
|
+
expect(config.excludeExamples).toBeUndefined();
|
|
11
|
+
expect(config.excludePaths).toBeUndefined();
|
|
12
|
+
expect(config.description).not.toContain('Multi-Tenant Mode');
|
|
13
|
+
});
|
|
14
|
+
it('should exclude company-related tags, schema properties, and examples when the company feature is off', ()=>{
|
|
15
|
+
const config = authSwaggerConfig({
|
|
16
|
+
enableCompanyFeature: false,
|
|
17
|
+
databaseMode: 'single'
|
|
18
|
+
});
|
|
19
|
+
expect(config.excludeTags).toEqual(expect.arrayContaining([
|
|
20
|
+
'Companies',
|
|
21
|
+
'Branches',
|
|
22
|
+
'User Permissions',
|
|
23
|
+
'Company Selection'
|
|
24
|
+
]));
|
|
25
|
+
expect(config.excludeSchemaProperties).toEqual(expect.arrayContaining([
|
|
26
|
+
expect.objectContaining({
|
|
27
|
+
schemaName: 'RegistrationDto'
|
|
28
|
+
})
|
|
29
|
+
]));
|
|
30
|
+
expect(config.excludeExamples).toEqual(expect.arrayContaining([
|
|
31
|
+
expect.objectContaining({
|
|
32
|
+
pathPattern: '/auth/login',
|
|
33
|
+
examples: [
|
|
34
|
+
'loginSuccessWithCompany',
|
|
35
|
+
'companySelection'
|
|
36
|
+
]
|
|
37
|
+
})
|
|
38
|
+
]));
|
|
39
|
+
// Email verification is still enabled by default, so its paths must not be excluded
|
|
40
|
+
expect(config.excludePaths).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
it('should exclude email-verification paths, schema properties, and examples when disabled', ()=>{
|
|
43
|
+
const config = authSwaggerConfig({
|
|
44
|
+
enableEmailVerification: false
|
|
45
|
+
});
|
|
46
|
+
expect(config.excludePaths).toEqual([
|
|
47
|
+
'/auth/forgot-password',
|
|
48
|
+
'/auth/reset-password',
|
|
49
|
+
'/auth/resend-verification',
|
|
50
|
+
'/auth/verify-email'
|
|
51
|
+
]);
|
|
52
|
+
expect(config.excludeSchemaProperties).toEqual(expect.arrayContaining([
|
|
53
|
+
expect.objectContaining({
|
|
54
|
+
schemaName: 'LoginResponseRequiresEmailVerificationDto'
|
|
55
|
+
})
|
|
56
|
+
]));
|
|
57
|
+
expect(config.excludeExamples).toEqual(expect.arrayContaining([
|
|
58
|
+
expect.objectContaining({
|
|
59
|
+
pathPattern: '/auth/login',
|
|
60
|
+
examples: [
|
|
61
|
+
'emailVerification'
|
|
62
|
+
]
|
|
63
|
+
})
|
|
64
|
+
]));
|
|
65
|
+
// Company feature is still enabled by default, so its tags must not be excluded
|
|
66
|
+
expect(config.excludeTags).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
it('should merge exclusions from both features when both are disabled', ()=>{
|
|
69
|
+
const config = authSwaggerConfig({
|
|
70
|
+
enableCompanyFeature: false,
|
|
71
|
+
enableEmailVerification: false
|
|
72
|
+
});
|
|
73
|
+
expect(config.excludeTags?.length).toBeGreaterThan(0);
|
|
74
|
+
expect(config.excludePaths?.length).toBeGreaterThan(0);
|
|
75
|
+
// Both feature's schema exclusions should be present together
|
|
76
|
+
const schemaNames = config.excludeSchemaProperties?.map((e)=>e.schemaName) ?? [];
|
|
77
|
+
expect(schemaNames).toContain('RegistrationDto');
|
|
78
|
+
expect(schemaNames).toContain('LoginResponseRequiresEmailVerificationDto');
|
|
79
|
+
// Both feature's example exclusions merge onto the same /auth/login path
|
|
80
|
+
const loginExclusions = config.excludeExamples?.filter((e)=>e.pathPattern === '/auth/login');
|
|
81
|
+
expect(loginExclusions).toHaveLength(2);
|
|
82
|
+
});
|
|
83
|
+
it('should mention multi-tenant mode in the description only for multi-tenant database mode', ()=>{
|
|
84
|
+
const multiTenant = authSwaggerConfig({
|
|
85
|
+
databaseMode: 'multi-tenant'
|
|
86
|
+
});
|
|
87
|
+
const single = authSwaggerConfig({
|
|
88
|
+
databaseMode: 'single'
|
|
89
|
+
});
|
|
90
|
+
expect(multiTenant.description).toContain('Multi-Tenant Mode');
|
|
91
|
+
expect(single.description).not.toContain('Multi-Tenant Mode');
|
|
92
|
+
});
|
|
93
|
+
it('should describe the multi-company login flow only when the company feature is enabled', ()=>{
|
|
94
|
+
const withCompany = authSwaggerConfig({
|
|
95
|
+
enableCompanyFeature: true
|
|
96
|
+
});
|
|
97
|
+
const withoutCompany = authSwaggerConfig({
|
|
98
|
+
enableCompanyFeature: false
|
|
99
|
+
});
|
|
100
|
+
expect(withCompany.description).toContain('Multi-Company Login');
|
|
101
|
+
expect(withCompany.description).toContain('/auth/switch-company');
|
|
102
|
+
expect(withoutCompany.description).not.toContain('Multi-Company Login');
|
|
103
|
+
expect(withoutCompany.description).not.toContain('/auth/switch-company');
|
|
104
|
+
});
|
|
105
|
+
it('should describe email verification endpoints only when enabled', ()=>{
|
|
106
|
+
const withVerification = authSwaggerConfig({
|
|
107
|
+
enableEmailVerification: true
|
|
108
|
+
});
|
|
109
|
+
const withoutVerification = authSwaggerConfig({
|
|
110
|
+
enableEmailVerification: false
|
|
111
|
+
});
|
|
112
|
+
expect(withVerification.description).toContain('/auth/forgot-password');
|
|
113
|
+
expect(withoutVerification.description).not.toContain('### Email Verification Endpoints');
|
|
114
|
+
});
|
|
115
|
+
it('should list company administration endpoints only when the company feature is enabled', ()=>{
|
|
116
|
+
const withCompany = authSwaggerConfig({
|
|
117
|
+
enableCompanyFeature: true
|
|
118
|
+
});
|
|
119
|
+
const withoutCompany = authSwaggerConfig({
|
|
120
|
+
enableCompanyFeature: false
|
|
121
|
+
});
|
|
122
|
+
expect(withCompany.description).toContain('/administration/company/*');
|
|
123
|
+
expect(withoutCompany.description).not.toContain('/administration/company/*');
|
|
124
|
+
expect(withoutCompany.description).toContain('/administration/users/*');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getAuthEntitiesByConfig } from './index';
|
|
2
|
+
import { AppUser } from './app-user.entity';
|
|
3
|
+
import { UserToken } from './user-token.entity';
|
|
4
|
+
import { PendingRegistration } from './pending-registration.entity';
|
|
5
|
+
import { Company } from './company.entity';
|
|
6
|
+
import { CompanyBranch } from './company-branch.entity';
|
|
7
|
+
import { UserCompanyPermission } from './user-company-permission.entity';
|
|
8
|
+
describe('getAuthEntitiesByConfig', ()=>{
|
|
9
|
+
it('should always include the base entities', ()=>{
|
|
10
|
+
const entities = getAuthEntitiesByConfig({
|
|
11
|
+
enableCompanyFeature: false,
|
|
12
|
+
enableEmailVerification: false
|
|
13
|
+
});
|
|
14
|
+
expect(entities).toEqual(expect.arrayContaining([
|
|
15
|
+
AppUser,
|
|
16
|
+
UserToken
|
|
17
|
+
]));
|
|
18
|
+
});
|
|
19
|
+
it('should include PendingRegistration when email verification is enabled', ()=>{
|
|
20
|
+
const entities = getAuthEntitiesByConfig({
|
|
21
|
+
enableEmailVerification: true
|
|
22
|
+
});
|
|
23
|
+
expect(entities).toContain(PendingRegistration);
|
|
24
|
+
});
|
|
25
|
+
it('should exclude PendingRegistration when email verification is disabled', ()=>{
|
|
26
|
+
const entities = getAuthEntitiesByConfig({
|
|
27
|
+
enableEmailVerification: false
|
|
28
|
+
});
|
|
29
|
+
expect(entities).not.toContain(PendingRegistration);
|
|
30
|
+
});
|
|
31
|
+
it('should include company entities when the company feature is enabled', ()=>{
|
|
32
|
+
const entities = getAuthEntitiesByConfig({
|
|
33
|
+
enableCompanyFeature: true
|
|
34
|
+
});
|
|
35
|
+
expect(entities).toEqual(expect.arrayContaining([
|
|
36
|
+
Company,
|
|
37
|
+
CompanyBranch,
|
|
38
|
+
UserCompanyPermission
|
|
39
|
+
]));
|
|
40
|
+
});
|
|
41
|
+
it('should exclude company entities when the company feature is disabled', ()=>{
|
|
42
|
+
const entities = getAuthEntitiesByConfig({
|
|
43
|
+
enableCompanyFeature: false
|
|
44
|
+
});
|
|
45
|
+
expect(entities).not.toEqual(expect.arrayContaining([
|
|
46
|
+
Company
|
|
47
|
+
]));
|
|
48
|
+
});
|
|
49
|
+
it('should default enableEmailVerification to true and enableCompanyFeature to false when omitted', ()=>{
|
|
50
|
+
const entities = getAuthEntitiesByConfig({});
|
|
51
|
+
expect(entities).toContain(PendingRegistration);
|
|
52
|
+
expect(entities).not.toContain(Company);
|
|
53
|
+
});
|
|
54
|
+
it('should include every entity group when both features are enabled', ()=>{
|
|
55
|
+
const entities = getAuthEntitiesByConfig({
|
|
56
|
+
enableCompanyFeature: true,
|
|
57
|
+
enableEmailVerification: true
|
|
58
|
+
});
|
|
59
|
+
expect(entities).toHaveLength(6);
|
|
60
|
+
expect(entities).toEqual(expect.arrayContaining([
|
|
61
|
+
AppUser,
|
|
62
|
+
UserToken,
|
|
63
|
+
PendingRegistration,
|
|
64
|
+
Company,
|
|
65
|
+
CompanyBranch,
|
|
66
|
+
UserCompanyPermission
|
|
67
|
+
]));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function toUserInfo(user) {
|
|
2
|
+
return {
|
|
3
|
+
id: user.id,
|
|
4
|
+
email: user.email,
|
|
5
|
+
name: user.name ?? undefined,
|
|
6
|
+
phone: user.phone ?? null,
|
|
7
|
+
profilePictureId: user.profilePictureId ?? null
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function toCompanyInfo(company) {
|
|
11
|
+
return {
|
|
12
|
+
id: company.id,
|
|
13
|
+
name: company.name,
|
|
14
|
+
slug: company.slug,
|
|
15
|
+
logoId: company.logoId ?? null
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function toBranchInfo(branch) {
|
|
19
|
+
return {
|
|
20
|
+
id: branch.id,
|
|
21
|
+
name: branch.name,
|
|
22
|
+
slug: branch.slug,
|
|
23
|
+
logoId: branch.logoId ?? null
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { toBranchInfo, toCompanyInfo, toUserInfo } from './auth-response.mapper';
|
|
2
|
+
function buildUser(overrides = {}) {
|
|
3
|
+
return {
|
|
4
|
+
id: 'user-uuid-1',
|
|
5
|
+
email: 'user@example.com',
|
|
6
|
+
name: 'Test User',
|
|
7
|
+
phone: null,
|
|
8
|
+
profilePictureId: null,
|
|
9
|
+
isActive: true,
|
|
10
|
+
emailVerified: false,
|
|
11
|
+
phoneVerified: false,
|
|
12
|
+
lastLoginAt: null,
|
|
13
|
+
additionalFields: null,
|
|
14
|
+
createdAt: new Date(),
|
|
15
|
+
updatedAt: new Date(),
|
|
16
|
+
deletedAt: null,
|
|
17
|
+
createdById: null,
|
|
18
|
+
updatedById: null,
|
|
19
|
+
deletedById: null,
|
|
20
|
+
password: null,
|
|
21
|
+
...overrides
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function buildCompany(overrides = {}) {
|
|
25
|
+
return {
|
|
26
|
+
id: 'company-uuid-1',
|
|
27
|
+
name: 'Acme Corp',
|
|
28
|
+
slug: 'acme-corp',
|
|
29
|
+
logoId: null,
|
|
30
|
+
readOnly: false,
|
|
31
|
+
address: null,
|
|
32
|
+
phone: null,
|
|
33
|
+
email: null,
|
|
34
|
+
website: null,
|
|
35
|
+
serial: null,
|
|
36
|
+
isActive: true,
|
|
37
|
+
settings: null,
|
|
38
|
+
additionalFields: null,
|
|
39
|
+
branches: [],
|
|
40
|
+
...overrides
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function buildBranch(overrides = {}) {
|
|
44
|
+
return {
|
|
45
|
+
id: 'branch-uuid-1',
|
|
46
|
+
name: 'Main Branch',
|
|
47
|
+
slug: 'main-branch',
|
|
48
|
+
logoId: null,
|
|
49
|
+
readOnly: false,
|
|
50
|
+
address: null,
|
|
51
|
+
phone: null,
|
|
52
|
+
email: null,
|
|
53
|
+
serial: null,
|
|
54
|
+
isActive: true,
|
|
55
|
+
additionalFields: null,
|
|
56
|
+
parentId: null,
|
|
57
|
+
parent: null,
|
|
58
|
+
children: [],
|
|
59
|
+
companyId: 'company-uuid-1',
|
|
60
|
+
company: buildCompany(),
|
|
61
|
+
...overrides
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
describe('auth-response.mapper', ()=>{
|
|
65
|
+
describe('toUserInfo', ()=>{
|
|
66
|
+
it('maps all user fields', ()=>{
|
|
67
|
+
const user = buildUser({
|
|
68
|
+
name: 'John Doe',
|
|
69
|
+
phone: '+123',
|
|
70
|
+
profilePictureId: 'pic-1'
|
|
71
|
+
});
|
|
72
|
+
const result = toUserInfo(user);
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
id: 'user-uuid-1',
|
|
75
|
+
email: 'user@example.com',
|
|
76
|
+
name: 'John Doe',
|
|
77
|
+
phone: '+123',
|
|
78
|
+
profilePictureId: 'pic-1'
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
it('defaults name to undefined and nullable fields to null when absent', ()=>{
|
|
82
|
+
const user = buildUser({
|
|
83
|
+
name: null,
|
|
84
|
+
phone: null,
|
|
85
|
+
profilePictureId: null
|
|
86
|
+
});
|
|
87
|
+
const result = toUserInfo(user);
|
|
88
|
+
expect(result.name).toBeUndefined();
|
|
89
|
+
expect(result.phone).toBeNull();
|
|
90
|
+
expect(result.profilePictureId).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
describe('toCompanyInfo', ()=>{
|
|
94
|
+
it('maps all company fields', ()=>{
|
|
95
|
+
const company = buildCompany({
|
|
96
|
+
logoId: 'logo-1'
|
|
97
|
+
});
|
|
98
|
+
const result = toCompanyInfo(company);
|
|
99
|
+
expect(result).toEqual({
|
|
100
|
+
id: 'company-uuid-1',
|
|
101
|
+
name: 'Acme Corp',
|
|
102
|
+
slug: 'acme-corp',
|
|
103
|
+
logoId: 'logo-1'
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
it('defaults logoId to null when absent', ()=>{
|
|
107
|
+
const company = buildCompany({
|
|
108
|
+
logoId: null
|
|
109
|
+
});
|
|
110
|
+
const result = toCompanyInfo(company);
|
|
111
|
+
expect(result.logoId).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('toBranchInfo', ()=>{
|
|
115
|
+
it('maps all branch fields', ()=>{
|
|
116
|
+
const branch = buildBranch({
|
|
117
|
+
logoId: 'logo-2'
|
|
118
|
+
});
|
|
119
|
+
const result = toBranchInfo(branch);
|
|
120
|
+
expect(result).toEqual({
|
|
121
|
+
id: 'branch-uuid-1',
|
|
122
|
+
name: 'Main Branch',
|
|
123
|
+
slug: 'main-branch',
|
|
124
|
+
logoId: 'logo-2'
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
it('defaults logoId to null when absent', ()=>{
|
|
128
|
+
const branch = buildBranch({
|
|
129
|
+
logoId: null
|
|
130
|
+
});
|
|
131
|
+
const result = toBranchInfo(branch);
|
|
132
|
+
expect(result.logoId).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function generateTokens(jwtService, authConfig, payload) {
|
|
2
|
+
const accessToken = jwtService.sign({
|
|
3
|
+
...payload,
|
|
4
|
+
type: 'access'
|
|
5
|
+
}, {
|
|
6
|
+
secret: authConfig.getJwtSecret(),
|
|
7
|
+
expiresIn: authConfig.getJwtExpiration(),
|
|
8
|
+
algorithm: 'HS256'
|
|
9
|
+
});
|
|
10
|
+
const refreshToken = jwtService.sign({
|
|
11
|
+
...payload,
|
|
12
|
+
type: 'refresh'
|
|
13
|
+
}, {
|
|
14
|
+
secret: authConfig.getRefreshTokenSecret(),
|
|
15
|
+
expiresIn: authConfig.getRefreshTokenExpiration(),
|
|
16
|
+
algorithm: 'HS256'
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
accessToken,
|
|
20
|
+
refreshToken
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function parseExpiryToMs(expiry) {
|
|
24
|
+
const match = expiry.match(/^(\d+)([smhd])$/);
|
|
25
|
+
if (!match) return 7 * 24 * 60 * 60 * 1000;
|
|
26
|
+
const value = parseInt(match[1], 10);
|
|
27
|
+
const unit = match[2];
|
|
28
|
+
switch(unit){
|
|
29
|
+
case 's':
|
|
30
|
+
return value * 1000;
|
|
31
|
+
case 'm':
|
|
32
|
+
return value * 60 * 1000;
|
|
33
|
+
case 'h':
|
|
34
|
+
return value * 60 * 60 * 1000;
|
|
35
|
+
case 'd':
|
|
36
|
+
return value * 24 * 60 * 60 * 1000;
|
|
37
|
+
default:
|
|
38
|
+
return 7 * 24 * 60 * 60 * 1000;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { generateTokens, parseExpiryToMs } from './token.helper';
|
|
2
|
+
describe('token.helper', ()=>{
|
|
3
|
+
describe('generateTokens', ()=>{
|
|
4
|
+
let mockJwtService;
|
|
5
|
+
let mockAuthConfig;
|
|
6
|
+
beforeEach(()=>{
|
|
7
|
+
mockJwtService = {
|
|
8
|
+
sign: jest.fn()
|
|
9
|
+
};
|
|
10
|
+
mockAuthConfig = {
|
|
11
|
+
getJwtSecret: jest.fn().mockReturnValue('jwt-secret'),
|
|
12
|
+
getJwtExpiration: jest.fn().mockReturnValue('1h'),
|
|
13
|
+
getRefreshTokenSecret: jest.fn().mockReturnValue('refresh-secret'),
|
|
14
|
+
getRefreshTokenExpiration: jest.fn().mockReturnValue('7d')
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
it('signs an access token and a refresh token with distinct secrets', ()=>{
|
|
18
|
+
mockJwtService.sign.mockReturnValueOnce('access-token-value').mockReturnValueOnce('refresh-token-value');
|
|
19
|
+
const result = generateTokens(mockJwtService, mockAuthConfig, {
|
|
20
|
+
id: 'user-1',
|
|
21
|
+
email: 'user@example.com',
|
|
22
|
+
type: 'access'
|
|
23
|
+
});
|
|
24
|
+
expect(result).toEqual({
|
|
25
|
+
accessToken: 'access-token-value',
|
|
26
|
+
refreshToken: 'refresh-token-value'
|
|
27
|
+
});
|
|
28
|
+
expect(mockJwtService.sign).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
|
29
|
+
id: 'user-1',
|
|
30
|
+
email: 'user@example.com',
|
|
31
|
+
type: 'access'
|
|
32
|
+
}), expect.objectContaining({
|
|
33
|
+
secret: 'jwt-secret',
|
|
34
|
+
expiresIn: '1h',
|
|
35
|
+
algorithm: 'HS256'
|
|
36
|
+
}));
|
|
37
|
+
expect(mockJwtService.sign).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
|
38
|
+
id: 'user-1',
|
|
39
|
+
email: 'user@example.com',
|
|
40
|
+
type: 'refresh'
|
|
41
|
+
}), expect.objectContaining({
|
|
42
|
+
secret: 'refresh-secret',
|
|
43
|
+
expiresIn: '7d',
|
|
44
|
+
algorithm: 'HS256'
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
it('includes companyId and branchId in the signed payload when provided', ()=>{
|
|
48
|
+
mockJwtService.sign.mockReturnValue('token');
|
|
49
|
+
generateTokens(mockJwtService, mockAuthConfig, {
|
|
50
|
+
id: 'user-1',
|
|
51
|
+
email: 'user@example.com',
|
|
52
|
+
type: 'access',
|
|
53
|
+
companyId: 'company-1',
|
|
54
|
+
branchId: 'branch-1'
|
|
55
|
+
});
|
|
56
|
+
expect(mockJwtService.sign).toHaveBeenCalledWith(expect.objectContaining({
|
|
57
|
+
companyId: 'company-1',
|
|
58
|
+
branchId: 'branch-1'
|
|
59
|
+
}), expect.anything());
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('parseExpiryToMs', ()=>{
|
|
63
|
+
it('parses seconds', ()=>{
|
|
64
|
+
expect(parseExpiryToMs('30s')).toBe(30 * 1000);
|
|
65
|
+
});
|
|
66
|
+
it('parses minutes', ()=>{
|
|
67
|
+
expect(parseExpiryToMs('15m')).toBe(15 * 60 * 1000);
|
|
68
|
+
});
|
|
69
|
+
it('parses hours', ()=>{
|
|
70
|
+
expect(parseExpiryToMs('2h')).toBe(2 * 60 * 60 * 1000);
|
|
71
|
+
});
|
|
72
|
+
it('parses days', ()=>{
|
|
73
|
+
expect(parseExpiryToMs('7d')).toBe(7 * 24 * 60 * 60 * 1000);
|
|
74
|
+
});
|
|
75
|
+
it('falls back to 7 days for an unparsable value', ()=>{
|
|
76
|
+
expect(parseExpiryToMs('not-a-duration')).toBe(7 * 24 * 60 * 60 * 1000);
|
|
77
|
+
});
|
|
78
|
+
it('falls back to 7 days for an unknown unit', ()=>{
|
|
79
|
+
expect(parseExpiryToMs('5y')).toBe(7 * 24 * 60 * 60 * 1000);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|