@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.
Files changed (71) hide show
  1. package/cjs/controllers/authentication.controller.spec.js +219 -0
  2. package/cjs/controllers/branch.controller.spec.js +134 -0
  3. package/cjs/controllers/company-selection.controller.spec.js +84 -0
  4. package/cjs/controllers/company.controller.spec.js +131 -0
  5. package/cjs/controllers/email-verification.controller.spec.js +142 -0
  6. package/cjs/controllers/user-permission.controller.spec.js +138 -0
  7. package/cjs/controllers/user.controller.spec.js +204 -0
  8. package/cjs/docs/auth-swagger.config.spec.js +130 -0
  9. package/cjs/entities/index.spec.js +73 -0
  10. package/cjs/helpers/auth-response.mapper.js +46 -0
  11. package/cjs/helpers/auth-response.mapper.spec.js +139 -0
  12. package/cjs/helpers/index.js +19 -0
  13. package/cjs/helpers/token.helper.js +58 -0
  14. package/cjs/helpers/token.helper.spec.js +86 -0
  15. package/cjs/index.js +1 -0
  16. package/cjs/interceptors/clear-token.interceptor.spec.js +107 -0
  17. package/cjs/interceptors/set-token.interceptor.spec.js +165 -0
  18. package/cjs/modules/auth.module.spec.js +243 -0
  19. package/cjs/services/auth-config.service.spec.js +97 -0
  20. package/cjs/services/auth-datasource.provider.spec.js +161 -0
  21. package/cjs/services/auth-email.service.spec.js +404 -0
  22. package/cjs/services/authentication.service.js +37 -109
  23. package/cjs/services/authentication.service.spec.js +904 -0
  24. package/cjs/services/branch.service.spec.js +154 -0
  25. package/cjs/services/company-selection-session.service.spec.js +90 -0
  26. package/cjs/services/company.service.spec.js +113 -0
  27. package/cjs/services/user-permission.service.spec.js +202 -0
  28. package/cjs/services/user.service.spec.js +704 -0
  29. package/cjs/strategies/jwt.strategy.js +1 -1
  30. package/cjs/strategies/jwt.strategy.spec.js +166 -0
  31. package/cjs/validators/password.validator.spec.js +161 -0
  32. package/entities/pending-registration.entity.d.ts +2 -7
  33. package/fesm/controllers/authentication.controller.spec.js +215 -0
  34. package/fesm/controllers/branch.controller.spec.js +130 -0
  35. package/fesm/controllers/company-selection.controller.spec.js +80 -0
  36. package/fesm/controllers/company.controller.spec.js +127 -0
  37. package/fesm/controllers/email-verification.controller.spec.js +138 -0
  38. package/fesm/controllers/user-permission.controller.spec.js +134 -0
  39. package/fesm/controllers/user.controller.spec.js +200 -0
  40. package/fesm/docs/auth-swagger.config.spec.js +126 -0
  41. package/fesm/entities/index.spec.js +69 -0
  42. package/fesm/helpers/auth-response.mapper.js +25 -0
  43. package/fesm/helpers/auth-response.mapper.spec.js +135 -0
  44. package/fesm/helpers/index.js +2 -0
  45. package/fesm/helpers/token.helper.js +40 -0
  46. package/fesm/helpers/token.helper.spec.js +82 -0
  47. package/fesm/index.js +1 -0
  48. package/fesm/interceptors/clear-token.interceptor.spec.js +103 -0
  49. package/fesm/interceptors/set-token.interceptor.spec.js +161 -0
  50. package/fesm/interfaces/authentication.interface.js +1 -1
  51. package/fesm/modules/auth.module.spec.js +239 -0
  52. package/fesm/services/auth-config.service.spec.js +93 -0
  53. package/fesm/services/auth-datasource.provider.spec.js +157 -0
  54. package/fesm/services/auth-email.service.spec.js +359 -0
  55. package/fesm/services/authentication.service.js +37 -109
  56. package/fesm/services/authentication.service.spec.js +859 -0
  57. package/fesm/services/branch.service.spec.js +150 -0
  58. package/fesm/services/company-selection-session.service.spec.js +86 -0
  59. package/fesm/services/company.service.spec.js +109 -0
  60. package/fesm/services/user-permission.service.spec.js +198 -0
  61. package/fesm/services/user.service.spec.js +659 -0
  62. package/fesm/strategies/jwt.strategy.js +1 -1
  63. package/fesm/strategies/jwt.strategy.spec.js +162 -0
  64. package/fesm/validators/password.validator.spec.js +157 -0
  65. package/helpers/auth-response.mapper.d.ts +22 -0
  66. package/helpers/index.d.ts +2 -0
  67. package/helpers/token.helper.d.ts +8 -0
  68. package/index.d.ts +1 -0
  69. package/interfaces/authentication.interface.d.ts +7 -0
  70. package/package.json +8 -3
  71. 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,2 @@
1
+ export * from './auth-response.mapper';
2
+ export * from './token.helper';
@@ -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
+ });