@trailmix-cms/cms 0.4.3 → 0.7.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 (266) hide show
  1. package/dist/auth.guard.d.ts +5 -13
  2. package/dist/auth.guard.d.ts.map +1 -1
  3. package/dist/auth.guard.js +23 -91
  4. package/dist/auth.guard.js.map +1 -1
  5. package/dist/collections/account.collection.d.ts +5 -3
  6. package/dist/collections/account.collection.d.ts.map +1 -1
  7. package/dist/collections/account.collection.js +15 -8
  8. package/dist/collections/account.collection.js.map +1 -1
  9. package/dist/collections/api-key.collection.d.ts +54 -0
  10. package/dist/collections/api-key.collection.d.ts.map +1 -0
  11. package/dist/collections/api-key.collection.js +142 -0
  12. package/dist/collections/api-key.collection.js.map +1 -0
  13. package/dist/collections/index.d.ts +4 -2
  14. package/dist/collections/index.d.ts.map +1 -1
  15. package/dist/collections/index.js +9 -5
  16. package/dist/collections/index.js.map +1 -1
  17. package/dist/collections/organization.collection.d.ts +20 -0
  18. package/dist/collections/organization.collection.d.ts.map +1 -0
  19. package/dist/collections/{file.collection.js → organization.collection.js} +17 -17
  20. package/dist/collections/organization.collection.js.map +1 -0
  21. package/dist/collections/role.collection.d.ts +32 -0
  22. package/dist/collections/role.collection.d.ts.map +1 -0
  23. package/dist/collections/role.collection.js +90 -0
  24. package/dist/collections/role.collection.js.map +1 -0
  25. package/dist/collections/security-audit.collection.d.ts +30 -0
  26. package/dist/collections/security-audit.collection.d.ts.map +1 -0
  27. package/dist/collections/security-audit.collection.js +79 -0
  28. package/dist/collections/security-audit.collection.js.map +1 -0
  29. package/dist/constants/cms-collection-names.d.ts +4 -2
  30. package/dist/constants/cms-collection-names.d.ts.map +1 -1
  31. package/dist/constants/cms-collection-names.js +4 -2
  32. package/dist/constants/cms-collection-names.js.map +1 -1
  33. package/dist/constants/provider-symbols.d.ts +10 -12
  34. package/dist/constants/provider-symbols.d.ts.map +1 -1
  35. package/dist/constants/provider-symbols.js +10 -12
  36. package/dist/constants/provider-symbols.js.map +1 -1
  37. package/dist/controllers/account.controller.d.ts +11 -15
  38. package/dist/controllers/account.controller.d.ts.map +1 -1
  39. package/dist/controllers/account.controller.js +69 -13
  40. package/dist/controllers/account.controller.js.map +1 -1
  41. package/dist/controllers/api-keys.controller.d.ts +13 -0
  42. package/dist/controllers/api-keys.controller.d.ts.map +1 -0
  43. package/dist/controllers/api-keys.controller.js +125 -0
  44. package/dist/controllers/api-keys.controller.js.map +1 -0
  45. package/dist/controllers/audit.controller.d.ts.map +1 -1
  46. package/dist/controllers/audit.controller.js +3 -3
  47. package/dist/controllers/audit.controller.js.map +1 -1
  48. package/dist/controllers/audits.controller.d.ts +10 -0
  49. package/dist/controllers/audits.controller.d.ts.map +1 -0
  50. package/dist/controllers/audits.controller.js +107 -0
  51. package/dist/controllers/audits.controller.js.map +1 -0
  52. package/dist/controllers/global-roles.controller.d.ts +16 -0
  53. package/dist/controllers/global-roles.controller.d.ts.map +1 -0
  54. package/dist/controllers/global-roles.controller.js +137 -0
  55. package/dist/controllers/global-roles.controller.js.map +1 -0
  56. package/dist/controllers/index.d.ts +6 -1
  57. package/dist/controllers/index.d.ts.map +1 -1
  58. package/dist/controllers/index.js +6 -1
  59. package/dist/controllers/index.js.map +1 -1
  60. package/dist/controllers/organization-roles.controller.d.ts +16 -0
  61. package/dist/controllers/organization-roles.controller.d.ts.map +1 -0
  62. package/dist/controllers/organization-roles.controller.js +145 -0
  63. package/dist/controllers/organization-roles.controller.js.map +1 -0
  64. package/dist/controllers/organizations.controller.d.ts +65 -0
  65. package/dist/controllers/organizations.controller.d.ts.map +1 -0
  66. package/dist/controllers/organizations.controller.js +140 -0
  67. package/dist/controllers/organizations.controller.js.map +1 -0
  68. package/dist/controllers/security-audits.controller.d.ts +11 -0
  69. package/dist/controllers/security-audits.controller.d.ts.map +1 -0
  70. package/dist/controllers/security-audits.controller.js +130 -0
  71. package/dist/controllers/security-audits.controller.js.map +1 -0
  72. package/dist/decorators/account.decorator.d.ts +1 -3
  73. package/dist/decorators/account.decorator.d.ts.map +1 -1
  74. package/dist/decorators/account.decorator.js +3 -10
  75. package/dist/decorators/account.decorator.js.map +1 -1
  76. package/dist/decorators/audit-context.decorator.d.ts +6 -0
  77. package/dist/decorators/audit-context.decorator.d.ts.map +1 -1
  78. package/dist/decorators/audit-context.decorator.js +12 -3
  79. package/dist/decorators/audit-context.decorator.js.map +1 -1
  80. package/dist/decorators/auth.decorator.d.ts +5 -3
  81. package/dist/decorators/auth.decorator.d.ts.map +1 -1
  82. package/dist/decorators/auth.decorator.js +38 -3
  83. package/dist/decorators/auth.decorator.js.map +1 -1
  84. package/dist/decorators/index.d.ts +4 -0
  85. package/dist/decorators/index.d.ts.map +1 -0
  86. package/dist/decorators/index.js +20 -0
  87. package/dist/decorators/index.js.map +1 -0
  88. package/dist/dto/account.dto.d.ts +33 -0
  89. package/dist/dto/account.dto.d.ts.map +1 -0
  90. package/dist/dto/account.dto.js +14 -0
  91. package/dist/dto/account.dto.js.map +1 -0
  92. package/dist/dto/api-key.dto.d.ts +89 -0
  93. package/dist/dto/api-key.dto.d.ts.map +1 -0
  94. package/dist/dto/api-key.dto.js +27 -0
  95. package/dist/dto/api-key.dto.js.map +1 -0
  96. package/dist/dto/audit.dto.d.ts +11 -5
  97. package/dist/dto/audit.dto.d.ts.map +1 -1
  98. package/dist/dto/audit.dto.js +1 -1
  99. package/dist/dto/audit.dto.js.map +1 -1
  100. package/dist/dto/global-role.dto.d.ts +99 -0
  101. package/dist/dto/global-role.dto.d.ts.map +1 -0
  102. package/dist/dto/global-role.dto.js +26 -0
  103. package/dist/dto/global-role.dto.js.map +1 -0
  104. package/dist/dto/organization-role.dto.d.ts +107 -0
  105. package/dist/dto/organization-role.dto.d.ts.map +1 -0
  106. package/dist/dto/organization-role.dto.js +26 -0
  107. package/dist/dto/organization-role.dto.js.map +1 -0
  108. package/dist/dto/organization.dto.d.ts +57 -0
  109. package/dist/dto/organization.dto.d.ts.map +1 -0
  110. package/dist/dto/organization.dto.js +32 -0
  111. package/dist/dto/organization.dto.js.map +1 -0
  112. package/dist/dto/security-audit.dto.d.ts +95 -0
  113. package/dist/dto/security-audit.dto.d.ts.map +1 -0
  114. package/dist/dto/security-audit.dto.js +26 -0
  115. package/dist/dto/security-audit.dto.js.map +1 -0
  116. package/dist/index.d.ts +7 -2
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +8 -3
  119. package/dist/index.js.map +1 -1
  120. package/dist/managers/global-role.manager.d.ts +42 -0
  121. package/dist/managers/global-role.manager.d.ts.map +1 -0
  122. package/dist/managers/global-role.manager.js +117 -0
  123. package/dist/managers/global-role.manager.js.map +1 -0
  124. package/dist/managers/index.d.ts +4 -0
  125. package/dist/managers/index.d.ts.map +1 -0
  126. package/dist/managers/index.js +20 -0
  127. package/dist/managers/index.js.map +1 -0
  128. package/dist/managers/organization-role.manager.d.ts +47 -0
  129. package/dist/managers/organization-role.manager.d.ts.map +1 -0
  130. package/dist/managers/organization-role.manager.js +218 -0
  131. package/dist/managers/organization-role.manager.js.map +1 -0
  132. package/dist/managers/organization.manager.d.ts +39 -0
  133. package/dist/managers/organization.manager.d.ts.map +1 -0
  134. package/dist/managers/organization.manager.js +196 -0
  135. package/dist/managers/organization.manager.js.map +1 -0
  136. package/dist/module.d.ts +92 -0
  137. package/dist/module.d.ts.map +1 -0
  138. package/dist/module.js +137 -0
  139. package/dist/module.js.map +1 -0
  140. package/dist/pipes/api-key.pipe.d.ts +8 -0
  141. package/dist/pipes/api-key.pipe.d.ts.map +1 -0
  142. package/dist/pipes/api-key.pipe.js +28 -0
  143. package/dist/pipes/api-key.pipe.js.map +1 -0
  144. package/dist/pipes/organization.pipe.d.ts +8 -0
  145. package/dist/pipes/organization.pipe.d.ts.map +1 -0
  146. package/dist/pipes/organization.pipe.js +28 -0
  147. package/dist/pipes/organization.pipe.js.map +1 -0
  148. package/dist/pipes/role.pipe.d.ts +8 -0
  149. package/dist/pipes/{file.pipe.d.ts.map → role.pipe.d.ts.map} +1 -1
  150. package/dist/pipes/{file.pipe.js → role.pipe.js} +8 -8
  151. package/dist/pipes/{file.pipe.js.map → role.pipe.js.map} +1 -1
  152. package/dist/services/account.service.d.ts +0 -2
  153. package/dist/services/account.service.d.ts.map +1 -1
  154. package/dist/services/account.service.js +1 -37
  155. package/dist/services/account.service.js.map +1 -1
  156. package/dist/services/api-key.service.d.ts +42 -0
  157. package/dist/services/api-key.service.d.ts.map +1 -0
  158. package/dist/services/api-key.service.js +306 -0
  159. package/dist/services/api-key.service.js.map +1 -0
  160. package/dist/services/auth.service.d.ts +40 -0
  161. package/dist/services/auth.service.d.ts.map +1 -0
  162. package/dist/services/auth.service.js +227 -0
  163. package/dist/services/auth.service.js.map +1 -0
  164. package/dist/services/authorization.service.d.ts +44 -9
  165. package/dist/services/authorization.service.d.ts.map +1 -1
  166. package/dist/services/authorization.service.js +107 -41
  167. package/dist/services/authorization.service.js.map +1 -1
  168. package/dist/services/feature.service.d.ts +23 -0
  169. package/dist/services/feature.service.d.ts.map +1 -0
  170. package/dist/services/feature.service.js +49 -0
  171. package/dist/services/feature.service.js.map +1 -0
  172. package/dist/services/global-role.service.d.ts +17 -0
  173. package/dist/services/global-role.service.d.ts.map +1 -0
  174. package/dist/services/global-role.service.js +99 -0
  175. package/dist/services/global-role.service.js.map +1 -0
  176. package/dist/services/index.d.ts +9 -0
  177. package/dist/services/index.d.ts.map +1 -0
  178. package/dist/services/index.js +25 -0
  179. package/dist/services/index.js.map +1 -0
  180. package/dist/services/organization-role.service.d.ts +33 -0
  181. package/dist/services/organization-role.service.d.ts.map +1 -0
  182. package/dist/services/organization-role.service.js +102 -0
  183. package/dist/services/organization-role.service.js.map +1 -0
  184. package/dist/services/organization.service.d.ts +29 -0
  185. package/dist/services/organization.service.d.ts.map +1 -0
  186. package/dist/services/organization.service.js +95 -0
  187. package/dist/services/organization.service.js.map +1 -0
  188. package/dist/types/feature-config.d.ts +9 -0
  189. package/dist/types/feature-config.d.ts.map +1 -0
  190. package/dist/types/feature-config.js +3 -0
  191. package/dist/types/feature-config.js.map +1 -0
  192. package/dist/types/hooks/auth-guard-hook.d.ts.map +1 -0
  193. package/dist/types/hooks/auth-guard-hook.js.map +1 -0
  194. package/dist/types/hooks/index.d.ts +3 -0
  195. package/dist/types/hooks/index.d.ts.map +1 -0
  196. package/dist/types/hooks/index.js +19 -0
  197. package/dist/types/hooks/index.js.map +1 -0
  198. package/dist/types/hooks/organization-delete-hook.d.ts +20 -0
  199. package/dist/types/hooks/organization-delete-hook.d.ts.map +1 -0
  200. package/dist/types/hooks/organization-delete-hook.js +3 -0
  201. package/dist/types/hooks/organization-delete-hook.js.map +1 -0
  202. package/dist/types/index.d.ts +5 -0
  203. package/dist/types/index.d.ts.map +1 -0
  204. package/dist/types/index.js +21 -0
  205. package/dist/types/index.js.map +1 -0
  206. package/dist/types/request-principal.d.ts +9 -0
  207. package/dist/types/request-principal.d.ts.map +1 -0
  208. package/dist/types/request-principal.js +3 -0
  209. package/dist/types/request-principal.js.map +1 -0
  210. package/dist/utils/provider-helpers.d.ts +6 -1
  211. package/dist/utils/provider-helpers.d.ts.map +1 -1
  212. package/dist/utils/provider-helpers.js +11 -1
  213. package/dist/utils/provider-helpers.js.map +1 -1
  214. package/package.json +59 -17
  215. package/test/unit/collections/api-key.collection.spec.ts +416 -0
  216. package/test/unit/managers/global-role.manager.spec.ts +269 -0
  217. package/test/unit/managers/organization-role.manager.spec.ts +632 -0
  218. package/test/unit/managers/organization.manager.spec.ts +395 -0
  219. package/test/unit/module.spec.ts +596 -0
  220. package/test/unit/services/account.service.spec.ts +90 -0
  221. package/test/unit/services/api-key.service.spec.ts +1244 -0
  222. package/test/unit/services/auth.service.spec.ts +790 -0
  223. package/test/unit/services/authorization.service.spec.ts +636 -0
  224. package/test/unit/services/feature.service.spec.ts +56 -0
  225. package/test/unit/services/global-role.service.spec.ts +289 -0
  226. package/test/unit/services/organization-role.service.spec.ts +300 -0
  227. package/test/unit/services/organization.service.spec.ts +385 -0
  228. package/test/utils/auth-guard.ts +114 -0
  229. package/test/utils/base.ts +16 -0
  230. package/test/utils/entities/account.ts +13 -0
  231. package/test/utils/entities/api-key.ts +15 -0
  232. package/test/utils/entities/audit.ts +18 -0
  233. package/test/utils/entities/index.ts +6 -0
  234. package/test/utils/entities/mapping.ts +20 -0
  235. package/test/utils/entities/organization.ts +13 -0
  236. package/test/utils/entities/role.ts +21 -0
  237. package/test/utils/entities/security-audit.ts +16 -0
  238. package/test/utils/index.ts +4 -0
  239. package/test/utils/models/audit-context.ts +10 -0
  240. package/test/utils/models/authorization.ts +7 -0
  241. package/test/utils/models/global-role.ts +22 -0
  242. package/test/utils/models/index.ts +5 -0
  243. package/test/utils/models/organization-role.ts +23 -0
  244. package/test/utils/models/publishable.ts +7 -0
  245. package/tsconfig.build.json +36 -0
  246. package/tsconfig.build.tsbuildinfo +1 -0
  247. package/dist/auth-guard-hook.d.ts.map +0 -1
  248. package/dist/auth-guard-hook.js.map +0 -1
  249. package/dist/cms.module.d.ts +0 -8
  250. package/dist/cms.module.d.ts.map +0 -1
  251. package/dist/cms.module.js +0 -44
  252. package/dist/cms.module.js.map +0 -1
  253. package/dist/cms.providers.d.ts +0 -120
  254. package/dist/cms.providers.d.ts.map +0 -1
  255. package/dist/cms.providers.js +0 -126
  256. package/dist/cms.providers.js.map +0 -1
  257. package/dist/collections/file.collection.d.ts +0 -21
  258. package/dist/collections/file.collection.d.ts.map +0 -1
  259. package/dist/collections/file.collection.js.map +0 -1
  260. package/dist/collections/text.collection.d.ts +0 -20
  261. package/dist/collections/text.collection.d.ts.map +0 -1
  262. package/dist/collections/text.collection.js +0 -56
  263. package/dist/collections/text.collection.js.map +0 -1
  264. package/dist/pipes/file.pipe.d.ts +0 -8
  265. /package/dist/{auth-guard-hook.d.ts → types/hooks/auth-guard-hook.d.ts} +0 -0
  266. /package/dist/{auth-guard-hook.js → types/hooks/auth-guard-hook.js} +0 -0
@@ -0,0 +1,416 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { Logger } from '@nestjs/common';
3
+ import { Collection, MongoServerError, ClientSession } from 'mongodb';
4
+ import { faker } from '@faker-js/faker';
5
+
6
+ import * as trailmixModels from '@trailmix-cms/models';
7
+ import { DatabaseService, Collections } from '@trailmix-cms/db';
8
+ import { Utils } from '@trailmix-cms/db';
9
+
10
+ import * as TestUtils from '../../utils';
11
+ import { ApiKeyCollection } from '@/collections';
12
+ import { CMSCollectionName } from '@/constants';
13
+
14
+ describe('ApiKeyCollection', () => {
15
+ let collection: ApiKeyCollection;
16
+ let mongoCollection: jest.Mocked<Collection<trailmixModels.ApiKey.Entity>>;
17
+ let databaseService: jest.Mocked<DatabaseService>;
18
+ let auditCollection: jest.Mocked<Collections.AuditCollection>;
19
+
20
+ beforeEach(async () => {
21
+ // Mock Logger methods to prevent console output during tests
22
+ jest.spyOn(Logger.prototype, 'log').mockImplementation();
23
+ jest.spyOn(Logger.prototype, 'error').mockImplementation();
24
+ jest.spyOn(Logger.prototype, 'warn').mockImplementation();
25
+ jest.spyOn(Logger.prototype, 'debug').mockImplementation();
26
+ jest.spyOn(Logger.prototype, 'verbose').mockImplementation();
27
+
28
+ const mockMongoCollection = {
29
+ createIndex: jest.fn().mockResolvedValue('api_key_1'),
30
+ insertOne: jest.fn(),
31
+ };
32
+
33
+ const mockSession = {} as ClientSession;
34
+ const mockDatabaseService = {
35
+ withTransaction: jest.fn((options, callback) => callback(mockSession)),
36
+ };
37
+
38
+ const mockAuditCollection = {
39
+ insertOne: jest.fn().mockResolvedValue(undefined),
40
+ };
41
+
42
+ const module: TestingModule = await Test.createTestingModule({
43
+ providers: [
44
+ ApiKeyCollection,
45
+ {
46
+ provide: Utils.buildCollectionToken(CMSCollectionName.ApiKey),
47
+ useValue: mockMongoCollection,
48
+ },
49
+ {
50
+ provide: DatabaseService,
51
+ useValue: mockDatabaseService,
52
+ },
53
+ {
54
+ provide: Collections.AuditCollection,
55
+ useValue: mockAuditCollection,
56
+ },
57
+ ],
58
+ }).compile();
59
+
60
+ collection = module.get<ApiKeyCollection>(ApiKeyCollection);
61
+ mongoCollection = module.get(Utils.buildCollectionToken(CMSCollectionName.ApiKey));
62
+ databaseService = module.get(DatabaseService);
63
+ auditCollection = module.get(Collections.AuditCollection);
64
+ });
65
+
66
+ afterEach(() => {
67
+ jest.clearAllMocks();
68
+ });
69
+
70
+ afterAll(() => {
71
+ // Restore Logger methods after all tests
72
+ jest.restoreAllMocks();
73
+ });
74
+
75
+ describe('onModuleInit', () => {
76
+ it('creates unique index on api_key field (ensuring API keys are unique)', async () => {
77
+ await collection.onModuleInit();
78
+
79
+ expect(mongoCollection.createIndex).toHaveBeenCalledWith(
80
+ { api_key: 1 },
81
+ { unique: true }
82
+ );
83
+ expect(Logger.prototype.verbose).toHaveBeenCalledWith(
84
+ `creating custom indexes for collection_${CMSCollectionName.ApiKey}`
85
+ );
86
+ });
87
+ });
88
+
89
+ describe('create', () => {
90
+ const accountEntity = TestUtils.Entities.createAccount();
91
+ const auditContext = TestUtils.Models.createAuditContext({
92
+ principal_id: accountEntity._id,
93
+ principal_type: trailmixModels.Principal.Account,
94
+ });
95
+
96
+ it('creates API key successfully on first attempt (ensuring basic creation works)', async () => {
97
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
98
+ name: faker.word.noun(),
99
+ scope_type: trailmixModels.ApiKeyScope.Account,
100
+ scope_id: accountEntity._id,
101
+ };
102
+
103
+ const createdApiKey = TestUtils.Entities.createApiKey({
104
+ ...params,
105
+ });
106
+
107
+ // Mock insertOne from AuditedCollection base class
108
+ jest.spyOn(collection, 'insertOne' as any).mockResolvedValueOnce(createdApiKey);
109
+
110
+ const result = await collection.create(params, auditContext);
111
+
112
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
113
+ { session: undefined },
114
+ expect.any(Function)
115
+ );
116
+ expect(collection.insertOne).toHaveBeenCalledWith(
117
+ expect.objectContaining({
118
+ name: params.name,
119
+ scope_type: params.scope_type,
120
+ scope_id: params.scope_id,
121
+ api_key: expect.stringMatching(/^[a-f0-9]{64}$/), // 32 bytes = 64 hex chars
122
+ }),
123
+ auditContext
124
+ );
125
+ expect(result).toEqual(createdApiKey);
126
+ expect(Logger.prototype.verbose).toHaveBeenCalledWith(
127
+ expect.stringContaining(`Successfully created APIKey with id ${createdApiKey._id} on attempt 1`)
128
+ );
129
+ });
130
+
131
+ it('creates API key with custom prefix (ensuring prefix is included in generated key)', async () => {
132
+ const prefix = 'test_prefix';
133
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
134
+ name: faker.word.noun(),
135
+ scope_type: trailmixModels.ApiKeyScope.Account,
136
+ scope_id: accountEntity._id,
137
+ };
138
+
139
+ const createdApiKey = TestUtils.Entities.createApiKey({
140
+ ...params,
141
+ api_key: `${prefix}_${faker.string.alphanumeric(64)}`,
142
+ });
143
+
144
+ jest.spyOn(collection, 'insertOne' as any).mockResolvedValueOnce(createdApiKey);
145
+
146
+ const result = await collection.create(params, auditContext, { prefix, maxRetries: 10, length: 32 });
147
+
148
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
149
+ { session: undefined },
150
+ expect.any(Function)
151
+ );
152
+ // Verify that insertOne was called with a document containing the prefix
153
+ expect(collection.insertOne).toHaveBeenCalledWith(
154
+ expect.objectContaining({
155
+ name: params.name,
156
+ scope_type: params.scope_type,
157
+ scope_id: params.scope_id,
158
+ api_key: expect.stringMatching(new RegExp(`^${prefix}_[a-f0-9]{64}$`)),
159
+ }),
160
+ auditContext
161
+ );
162
+ expect(result).toEqual(createdApiKey);
163
+ });
164
+
165
+ it('creates API key with custom length (ensuring custom length is respected)', async () => {
166
+ const customLength = 16;
167
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
168
+ name: faker.word.noun(),
169
+ scope_type: trailmixModels.ApiKeyScope.Account,
170
+ scope_id: accountEntity._id,
171
+ };
172
+
173
+ const createdApiKey = TestUtils.Entities.createApiKey({
174
+ ...params,
175
+ });
176
+
177
+ jest.spyOn(collection, 'insertOne' as any).mockResolvedValueOnce(createdApiKey);
178
+
179
+ const result = await collection.create(params, auditContext, { maxRetries: 10, length: customLength });
180
+
181
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
182
+ { session: undefined },
183
+ expect.any(Function)
184
+ );
185
+ // Verify that insertOne was called with a document containing the correct length API key
186
+ expect(collection.insertOne).toHaveBeenCalledWith(
187
+ expect.objectContaining({
188
+ name: params.name,
189
+ scope_type: params.scope_type,
190
+ scope_id: params.scope_id,
191
+ api_key: expect.stringMatching(/^[a-f0-9]{32}$/), // 16 bytes = 32 hex chars
192
+ }),
193
+ auditContext
194
+ );
195
+ expect(result).toEqual(createdApiKey);
196
+ });
197
+
198
+ it('retries with new API key when duplicate key error occurs (ensuring retry logic handles duplicates within same transaction)', async () => {
199
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
200
+ name: faker.word.noun(),
201
+ scope_type: trailmixModels.ApiKeyScope.Account,
202
+ scope_id: accountEntity._id,
203
+ };
204
+
205
+ const duplicateError = new MongoServerError({ message: 'Duplicate key error' });
206
+ duplicateError.code = 11000;
207
+
208
+ const createdApiKey = TestUtils.Entities.createApiKey({
209
+ ...params,
210
+ });
211
+
212
+ // Mock insertOne from AuditedCollection base class to fail first time with duplicate, then succeed
213
+ // The retry happens within the same transaction callback
214
+ jest.spyOn(collection, 'insertOne' as any)
215
+ .mockImplementationOnce(() => Promise.reject(duplicateError))
216
+ .mockImplementationOnce(() => Promise.resolve(createdApiKey));
217
+
218
+ const result = await collection.create(params, auditContext, { maxRetries: 10, length: 32 });
219
+
220
+ // Should only call withTransaction once - retries happen inside the callback
221
+ expect(databaseService.withTransaction).toHaveBeenCalledTimes(1);
222
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
223
+ { session: undefined },
224
+ expect.any(Function)
225
+ );
226
+ expect(Logger.prototype.warn).toHaveBeenCalledWith(
227
+ 'Duplicate API key detected on attempt 1, retrying with new key...'
228
+ );
229
+ expect(Logger.prototype.verbose).toHaveBeenCalledWith(
230
+ expect.stringContaining(`Successfully created APIKey with id ${createdApiKey._id} on attempt 2`)
231
+ );
232
+ expect(result).toEqual(createdApiKey);
233
+ });
234
+
235
+ it('retries multiple times when duplicate key errors occur (ensuring multiple retries work within same transaction)', async () => {
236
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
237
+ name: faker.word.noun(),
238
+ scope_type: trailmixModels.ApiKeyScope.Account,
239
+ scope_id: accountEntity._id,
240
+ };
241
+
242
+ const duplicateError = new MongoServerError({ message: 'Duplicate key error' });
243
+ duplicateError.code = 11000;
244
+
245
+ const createdApiKey = TestUtils.Entities.createApiKey({
246
+ ...params,
247
+ });
248
+
249
+ // Mock insertOne to fail twice with duplicate, then succeed
250
+ // All retries happen within the same transaction callback
251
+ jest.spyOn(collection, 'insertOne' as any)
252
+ .mockImplementationOnce(() => Promise.reject(duplicateError))
253
+ .mockImplementationOnce(() => Promise.reject(duplicateError))
254
+ .mockImplementationOnce(() => Promise.resolve(createdApiKey));
255
+
256
+ const result = await collection.create(params, auditContext, { maxRetries: 10, length: 32 });
257
+
258
+ // Should only call withTransaction once - all retries happen inside the callback
259
+ expect(databaseService.withTransaction).toHaveBeenCalledTimes(1);
260
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
261
+ { session: undefined },
262
+ expect.any(Function)
263
+ );
264
+ expect(Logger.prototype.warn).toHaveBeenCalledTimes(2);
265
+ expect(Logger.prototype.warn).toHaveBeenNthCalledWith(1, 'Duplicate API key detected on attempt 1, retrying with new key...');
266
+ expect(Logger.prototype.warn).toHaveBeenNthCalledWith(2, 'Duplicate API key detected on attempt 2, retrying with new key...');
267
+ expect(Logger.prototype.verbose).toHaveBeenCalledWith(
268
+ expect.stringContaining(`Successfully created APIKey with id ${createdApiKey._id} on attempt 3`)
269
+ );
270
+ expect(result).toEqual(createdApiKey);
271
+ });
272
+
273
+ it('throws error when max retries exceeded due to duplicate keys (ensuring max retries limit is enforced within transaction)', async () => {
274
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
275
+ name: faker.word.noun(),
276
+ scope_type: trailmixModels.ApiKeyScope.Account,
277
+ scope_id: accountEntity._id,
278
+ };
279
+
280
+ const duplicateError = new MongoServerError({ message: 'Duplicate key error' });
281
+ duplicateError.code = 11000;
282
+
283
+ const maxRetries = 3;
284
+ // Mock insertOne to always fail with duplicate error
285
+ jest.spyOn(collection, 'insertOne' as any).mockImplementation(() => Promise.reject(duplicateError));
286
+
287
+ await expect(
288
+ collection.create(params, auditContext, { maxRetries, length: 32 })
289
+ ).rejects.toThrow(`Failed to create APIKey: generated ${maxRetries} duplicate API keys. This is extremely unlikely.`);
290
+
291
+ // Should only call withTransaction once - all retries happen inside the callback
292
+ expect(databaseService.withTransaction).toHaveBeenCalledTimes(1);
293
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
294
+ { session: undefined },
295
+ expect.any(Function)
296
+ );
297
+ expect(Logger.prototype.warn).toHaveBeenCalledTimes(maxRetries);
298
+ expect(Logger.prototype.error).toHaveBeenCalledWith(
299
+ `Failed to create APIKey after ${maxRetries} attempts due to duplicate keys`
300
+ );
301
+ });
302
+
303
+ it('throws non-duplicate errors immediately without retrying (ensuring non-duplicate errors are not retried)', async () => {
304
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
305
+ name: faker.word.noun(),
306
+ scope_type: trailmixModels.ApiKeyScope.Account,
307
+ scope_id: accountEntity._id,
308
+ };
309
+
310
+ const otherError = new Error('Some other database error');
311
+ // Mock insertOne to throw non-duplicate error
312
+ jest.spyOn(collection, 'insertOne' as any).mockImplementationOnce(() => Promise.reject(otherError));
313
+
314
+ await expect(
315
+ collection.create(params, auditContext, { maxRetries: 10, length: 32 })
316
+ ).rejects.toThrow('Some other database error');
317
+
318
+ expect(databaseService.withTransaction).toHaveBeenCalledTimes(1);
319
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
320
+ { session: undefined },
321
+ expect.any(Function)
322
+ );
323
+ expect(Logger.prototype.warn).not.toHaveBeenCalled();
324
+ expect(Logger.prototype.error).toHaveBeenCalledWith(`Failed to create APIKey: ${otherError}`);
325
+ });
326
+
327
+ it('throws non-duplicate MongoServerError immediately without retrying (ensuring only duplicate errors trigger retries)', async () => {
328
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
329
+ name: faker.word.noun(),
330
+ scope_type: trailmixModels.ApiKeyScope.Account,
331
+ scope_id: accountEntity._id,
332
+ };
333
+
334
+ const mongoError = new MongoServerError({ message: 'Some MongoDB error' });
335
+ mongoError.code = 11001; // Different error code, not a duplicate key error
336
+
337
+ // Mock insertOne to throw non-duplicate MongoServerError
338
+ jest.spyOn(collection, 'insertOne' as any).mockImplementationOnce(() => Promise.reject(mongoError));
339
+
340
+ await expect(
341
+ collection.create(params, auditContext, { maxRetries: 10, length: 32 })
342
+ ).rejects.toThrow(mongoError);
343
+
344
+ expect(databaseService.withTransaction).toHaveBeenCalledTimes(1);
345
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
346
+ { session: undefined },
347
+ expect.any(Function)
348
+ );
349
+ expect(Logger.prototype.warn).not.toHaveBeenCalled();
350
+ expect(Logger.prototype.error).toHaveBeenCalledWith(`Failed to create APIKey: ${mongoError}`);
351
+ });
352
+
353
+ it('uses default options when not provided (ensuring default values are used)', async () => {
354
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
355
+ name: faker.word.noun(),
356
+ scope_type: trailmixModels.ApiKeyScope.Account,
357
+ scope_id: accountEntity._id,
358
+ };
359
+
360
+ const createdApiKey = TestUtils.Entities.createApiKey({
361
+ ...params,
362
+ });
363
+
364
+ jest.spyOn(collection, 'insertOne' as any).mockResolvedValueOnce(createdApiKey);
365
+
366
+ const result = await collection.create(params, auditContext);
367
+
368
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
369
+ { session: undefined },
370
+ expect.any(Function)
371
+ );
372
+ expect(result).toEqual(createdApiKey);
373
+ });
374
+
375
+ it('passes session parameter to withTransaction when provided (ensuring session is correctly propagated)', async () => {
376
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
377
+ name: faker.word.noun(),
378
+ scope_type: trailmixModels.ApiKeyScope.Account,
379
+ scope_id: accountEntity._id,
380
+ };
381
+
382
+ const createdApiKey = TestUtils.Entities.createApiKey({
383
+ ...params,
384
+ });
385
+
386
+ const providedSession = {} as ClientSession;
387
+ jest.spyOn(collection, 'insertOne' as any).mockResolvedValueOnce(createdApiKey);
388
+
389
+ const result = await collection.create(params, auditContext, { maxRetries: 10, length: 32 }, providedSession);
390
+
391
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
392
+ { session: providedSession },
393
+ expect.any(Function)
394
+ );
395
+ expect(result).toEqual(createdApiKey);
396
+ });
397
+
398
+ it('throws unexpected error when maxRetries is 0 (ensuring fallback error path is covered)', async () => {
399
+ const params: Omit<trailmixModels.ApiKey.Entity, '_id' | 'created_at' | 'updated_at' | 'api_key'> = {
400
+ name: faker.word.noun(),
401
+ scope_type: trailmixModels.ApiKeyScope.Account,
402
+ scope_id: accountEntity._id,
403
+ };
404
+
405
+ // When maxRetries is 0, the while loop won't execute, and we'll hit the fallback error
406
+ await expect(
407
+ collection.create(params, auditContext, { maxRetries: 0, length: 32 })
408
+ ).rejects.toThrow('Unexpected error in create method');
409
+
410
+ expect(databaseService.withTransaction).toHaveBeenCalledWith(
411
+ { session: undefined },
412
+ expect.any(Function)
413
+ );
414
+ });
415
+ });
416
+ });
@@ -0,0 +1,269 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { BadRequestException, ForbiddenException, NotFoundException, Logger } from '@nestjs/common';
3
+ import { ObjectId } from 'mongodb';
4
+ import * as trailmixModels from '@trailmix-cms/models';
5
+
6
+ import * as TestUtils from '../../utils';
7
+
8
+ import { GlobalRoleManager } from '@/managers';
9
+ import { GlobalRoleService, AuthorizationService } from '@/services';
10
+ import { SecurityAuditCollection } from '@/collections';
11
+ import { RequestPrincipal } from '@/types';
12
+ import { createAuditContextForPrincipal } from '@/decorators/audit-context.decorator';
13
+
14
+ describe('GlobalRoleManager', () => {
15
+ let manager: GlobalRoleManager;
16
+ let globalRoleService: jest.Mocked<GlobalRoleService>;
17
+ let authorizationService: jest.Mocked<AuthorizationService>;
18
+ let securityAuditCollection: jest.Mocked<SecurityAuditCollection>;
19
+
20
+ const accountEntity = TestUtils.Entities.createAccount();
21
+ const accountPrincipal: RequestPrincipal = {
22
+ principal_type: trailmixModels.Principal.Account,
23
+ entity: accountEntity,
24
+ };
25
+ const auditContext = createAuditContextForPrincipal(accountPrincipal);
26
+
27
+ beforeEach(async () => {
28
+ // Mock Logger methods to prevent console output during tests
29
+ jest.spyOn(Logger.prototype, 'log').mockImplementation();
30
+ jest.spyOn(Logger.prototype, 'error').mockImplementation();
31
+ jest.spyOn(Logger.prototype, 'warn').mockImplementation();
32
+ jest.spyOn(Logger.prototype, 'debug').mockImplementation();
33
+ jest.spyOn(Logger.prototype, 'verbose').mockImplementation();
34
+
35
+ const mockGlobalRoleService = {
36
+ insertOne: jest.fn(),
37
+ find: jest.fn(),
38
+ findOne: jest.fn(),
39
+ deleteOne: jest.fn(),
40
+ };
41
+
42
+ const mockAuthorizationService = {
43
+ isGlobalAdmin: jest.fn(),
44
+ };
45
+
46
+ const mockSecurityAuditCollection = {
47
+ insertOne: jest.fn().mockResolvedValue(undefined),
48
+ };
49
+
50
+ const module: TestingModule = await Test.createTestingModule({
51
+ providers: [
52
+ GlobalRoleManager,
53
+ {
54
+ provide: GlobalRoleService,
55
+ useValue: mockGlobalRoleService,
56
+ },
57
+ {
58
+ provide: AuthorizationService,
59
+ useValue: mockAuthorizationService,
60
+ },
61
+ {
62
+ provide: SecurityAuditCollection,
63
+ useValue: mockSecurityAuditCollection,
64
+ },
65
+ ],
66
+ }).compile();
67
+
68
+ manager = module.get<GlobalRoleManager>(GlobalRoleManager);
69
+ globalRoleService = module.get(GlobalRoleService);
70
+ authorizationService = module.get(AuthorizationService);
71
+ securityAuditCollection = module.get(SecurityAuditCollection);
72
+ });
73
+
74
+ afterEach(() => {
75
+ jest.clearAllMocks();
76
+ });
77
+
78
+ afterAll(() => {
79
+ jest.restoreAllMocks();
80
+ });
81
+
82
+ describe('insertOne', () => {
83
+ const params = {
84
+ principal_id: new ObjectId(),
85
+ principal_type: trailmixModels.Principal.Account,
86
+ role: trailmixModels.RoleValue.Admin,
87
+ };
88
+
89
+ it('successfully creates a global role when user is global admin and role does not exist (ensuring global role creation works)', async () => {
90
+ const globalRole = TestUtils.Models.createGlobalRoleModel(params);
91
+ authorizationService.isGlobalAdmin.mockResolvedValue(true);
92
+ globalRoleService.findOne.mockResolvedValue(null);
93
+ globalRoleService.insertOne.mockResolvedValue(globalRole);
94
+
95
+ const result = await manager.insertOne(params, accountPrincipal, auditContext);
96
+
97
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalledWith(
98
+ accountEntity._id,
99
+ accountPrincipal.principal_type,
100
+ );
101
+ expect(globalRoleService.findOne).toHaveBeenCalledWith(params);
102
+ expect(globalRoleService.insertOne).toHaveBeenCalledWith(params, auditContext);
103
+ expect(result).toEqual(globalRole);
104
+ expect(securityAuditCollection.insertOne).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it('throws ForbiddenException when user is not global admin (ensuring only global admins can create global roles)', async () => {
108
+ authorizationService.isGlobalAdmin.mockResolvedValue(false);
109
+
110
+ await expect(
111
+ manager.insertOne(params, accountPrincipal, auditContext)
112
+ ).rejects.toThrow(ForbiddenException);
113
+
114
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalled();
115
+ expect(globalRoleService.findOne).not.toHaveBeenCalled();
116
+ expect(globalRoleService.insertOne).not.toHaveBeenCalled();
117
+ expect(securityAuditCollection.insertOne).toHaveBeenCalledWith({
118
+ event_type: trailmixModels.SecurityAuditEventType.UnauthorizedAccess,
119
+ principal_id: accountEntity._id,
120
+ principal_type: accountPrincipal.principal_type,
121
+ message: 'Insufficient permissions to create global role',
122
+ source: GlobalRoleManager.name,
123
+ });
124
+ });
125
+
126
+ it('throws BadRequestException when role already exists (ensuring role uniqueness is enforced)', async () => {
127
+ const existingRole = TestUtils.Models.createGlobalRoleModel(params);
128
+ authorizationService.isGlobalAdmin.mockResolvedValue(true);
129
+ globalRoleService.findOne.mockResolvedValue(existingRole);
130
+
131
+ await expect(
132
+ manager.insertOne(params, accountPrincipal, auditContext)
133
+ ).rejects.toThrow(BadRequestException);
134
+
135
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalled();
136
+ expect(globalRoleService.findOne).toHaveBeenCalledWith(params);
137
+ expect(globalRoleService.insertOne).not.toHaveBeenCalled();
138
+ });
139
+ });
140
+
141
+ describe('find', () => {
142
+ const query = {
143
+ principal_id: new ObjectId(),
144
+ };
145
+
146
+ it('successfully finds global roles when user is global admin (ensuring global role retrieval works)', async () => {
147
+ const globalRoles = [
148
+ TestUtils.Models.createGlobalRoleModel(),
149
+ TestUtils.Models.createGlobalRoleModel(),
150
+ ];
151
+ authorizationService.isGlobalAdmin.mockResolvedValue(true);
152
+ globalRoleService.find.mockResolvedValue(globalRoles);
153
+
154
+ const result = await manager.find(query, accountPrincipal);
155
+
156
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalledWith(
157
+ accountEntity._id,
158
+ accountPrincipal.principal_type,
159
+ );
160
+ expect(globalRoleService.find).toHaveBeenCalledWith(query);
161
+ expect(result).toEqual(globalRoles);
162
+ });
163
+
164
+ it('throws ForbiddenException when user is not global admin (ensuring only global admins can find global roles)', async () => {
165
+ authorizationService.isGlobalAdmin.mockResolvedValue(false);
166
+
167
+ await expect(
168
+ manager.find(query, accountPrincipal)
169
+ ).rejects.toThrow(ForbiddenException);
170
+
171
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalled();
172
+ expect(globalRoleService.find).not.toHaveBeenCalled();
173
+ expect(securityAuditCollection.insertOne).toHaveBeenCalledWith({
174
+ event_type: trailmixModels.SecurityAuditEventType.UnauthorizedAccess,
175
+ principal_id: accountEntity._id,
176
+ principal_type: accountPrincipal.principal_type,
177
+ message: 'Insufficient permissions to find global roles',
178
+ source: GlobalRoleManager.name,
179
+ });
180
+ });
181
+ });
182
+
183
+ describe('get', () => {
184
+ const roleId = new ObjectId();
185
+
186
+ it('successfully gets a global role when user is global admin and role exists (ensuring global role retrieval works)', async () => {
187
+ const globalRole = TestUtils.Models.createGlobalRoleModel({ _id: roleId });
188
+ authorizationService.isGlobalAdmin.mockResolvedValue(true);
189
+ globalRoleService.findOne.mockResolvedValue(globalRole);
190
+
191
+ const result = await manager.get(roleId, accountPrincipal);
192
+
193
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalledWith(
194
+ accountEntity._id,
195
+ accountPrincipal.principal_type,
196
+ );
197
+ expect(globalRoleService.findOne).toHaveBeenCalledWith({ _id: roleId });
198
+ expect(result).toEqual(globalRole);
199
+ });
200
+
201
+ it('throws NotFoundException when role does not exist (ensuring role existence is validated)', async () => {
202
+ authorizationService.isGlobalAdmin.mockResolvedValue(true);
203
+ globalRoleService.findOne.mockResolvedValue(null);
204
+
205
+ await expect(
206
+ manager.get(roleId, accountPrincipal)
207
+ ).rejects.toThrow(NotFoundException);
208
+
209
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalled();
210
+ expect(globalRoleService.findOne).toHaveBeenCalledWith({ _id: roleId });
211
+ });
212
+
213
+ it('throws ForbiddenException when user is not global admin (ensuring only global admins can get global roles)', async () => {
214
+ authorizationService.isGlobalAdmin.mockResolvedValue(false);
215
+
216
+ await expect(
217
+ manager.get(roleId, accountPrincipal)
218
+ ).rejects.toThrow(ForbiddenException);
219
+
220
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalled();
221
+ expect(globalRoleService.findOne).not.toHaveBeenCalled();
222
+ });
223
+ });
224
+
225
+ describe('deleteOne', () => {
226
+ const roleId = new ObjectId();
227
+
228
+ it('successfully deletes a global role when user is global admin and role exists (ensuring global role deletion works)', async () => {
229
+ const globalRole = TestUtils.Models.createGlobalRoleModel({ _id: roleId });
230
+ authorizationService.isGlobalAdmin.mockResolvedValue(true);
231
+ globalRoleService.findOne.mockResolvedValue(globalRole);
232
+ globalRoleService.deleteOne.mockResolvedValue(undefined);
233
+
234
+ await manager.deleteOne(roleId, accountPrincipal, auditContext);
235
+
236
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalledWith(
237
+ accountEntity._id,
238
+ accountPrincipal.principal_type,
239
+ );
240
+ expect(globalRoleService.findOne).toHaveBeenCalledWith({ _id: roleId });
241
+ expect(globalRoleService.deleteOne).toHaveBeenCalledWith(roleId, auditContext);
242
+ });
243
+
244
+ it('throws NotFoundException when role does not exist (ensuring role existence is validated)', async () => {
245
+ authorizationService.isGlobalAdmin.mockResolvedValue(true);
246
+ globalRoleService.findOne.mockResolvedValue(null);
247
+
248
+ await expect(
249
+ manager.deleteOne(roleId, accountPrincipal, auditContext)
250
+ ).rejects.toThrow(NotFoundException);
251
+
252
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalled();
253
+ expect(globalRoleService.findOne).toHaveBeenCalledWith({ _id: roleId });
254
+ expect(globalRoleService.deleteOne).not.toHaveBeenCalled();
255
+ });
256
+
257
+ it('throws ForbiddenException when user is not global admin (ensuring only global admins can delete global roles)', async () => {
258
+ authorizationService.isGlobalAdmin.mockResolvedValue(false);
259
+
260
+ await expect(
261
+ manager.deleteOne(roleId, accountPrincipal, auditContext)
262
+ ).rejects.toThrow(ForbiddenException);
263
+
264
+ expect(authorizationService.isGlobalAdmin).toHaveBeenCalled();
265
+ expect(globalRoleService.findOne).not.toHaveBeenCalled();
266
+ expect(globalRoleService.deleteOne).not.toHaveBeenCalled();
267
+ });
268
+ });
269
+ });