@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,385 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { NotFoundException, Logger } from '@nestjs/common';
3
+ import { ObjectId, ClientSession } from 'mongodb';
4
+
5
+ import * as trailmixModels from '@trailmix-cms/models';
6
+
7
+ import * as TestUtils from '../../utils';
8
+
9
+ import { OrganizationService } from '@/services';
10
+ import { OrganizationCollection, RoleCollection } from '@/collections';
11
+ import { OrganizationRoleService } from '@/services/organization-role.service';
12
+ import { DatabaseService } from '@trailmix-cms/db';
13
+ import { OrganizationDeleteHook } from '@/types/hooks/organization-delete-hook';
14
+ import { PROVIDER_SYMBOLS } from '@/constants';
15
+ import { createAuditContextForPrincipal } from '@/decorators/audit-context.decorator';
16
+ import { RequestPrincipal } from '@/types';
17
+
18
+ describe('OrganizationService', () => {
19
+ let service: OrganizationService;
20
+ let organizationCollection: jest.Mocked<OrganizationCollection>;
21
+ let roleCollection: jest.Mocked<RoleCollection>;
22
+ let organizationRoleService: jest.Mocked<OrganizationRoleService>;
23
+ let databaseService: jest.Mocked<DatabaseService>;
24
+ let organizationDeleteHook: jest.Mocked<OrganizationDeleteHook> | undefined;
25
+
26
+ beforeEach(async () => {
27
+ // Mock Logger methods to prevent console output during tests
28
+ jest.spyOn(Logger.prototype, 'log').mockImplementation();
29
+ jest.spyOn(Logger.prototype, 'error').mockImplementation();
30
+ jest.spyOn(Logger.prototype, 'warn').mockImplementation();
31
+ jest.spyOn(Logger.prototype, 'debug').mockImplementation();
32
+ jest.spyOn(Logger.prototype, 'verbose').mockImplementation();
33
+
34
+ const mockOrganizationCollection = {
35
+ get: jest.fn(),
36
+ deleteOne: jest.fn(),
37
+ };
38
+
39
+ const mockRoleCollection = {
40
+ deleteOne: jest.fn(),
41
+ };
42
+
43
+ const mockOrganizationRoleService = {
44
+ find: jest.fn(),
45
+ };
46
+
47
+ const mockDatabaseService = {
48
+ withTransaction: jest.fn(),
49
+ };
50
+
51
+ const module: TestingModule = await Test.createTestingModule({
52
+ providers: [
53
+ OrganizationService,
54
+ {
55
+ provide: OrganizationCollection,
56
+ useValue: mockOrganizationCollection,
57
+ },
58
+ {
59
+ provide: RoleCollection,
60
+ useValue: mockRoleCollection,
61
+ },
62
+ {
63
+ provide: OrganizationRoleService,
64
+ useValue: mockOrganizationRoleService,
65
+ },
66
+ {
67
+ provide: DatabaseService,
68
+ useValue: mockDatabaseService,
69
+ },
70
+ ],
71
+ }).compile();
72
+
73
+ service = module.get<OrganizationService>(OrganizationService);
74
+ organizationCollection = module.get(OrganizationCollection);
75
+ roleCollection = module.get(RoleCollection);
76
+ organizationRoleService = module.get(OrganizationRoleService);
77
+ databaseService = module.get(DatabaseService);
78
+ });
79
+
80
+ afterEach(() => {
81
+ jest.clearAllMocks();
82
+ });
83
+
84
+ afterAll(() => {
85
+ // Restore Logger methods after all tests
86
+ jest.restoreAllMocks();
87
+ });
88
+
89
+ describe('deleteOrganization', () => {
90
+ const organizationId = new ObjectId();
91
+ const accountEntity = TestUtils.Entities.createAccount();
92
+ const accountPrincipal: RequestPrincipal = {
93
+ principal_type: trailmixModels.Principal.Account,
94
+ entity: accountEntity,
95
+ };
96
+ const auditContext = createAuditContextForPrincipal(accountPrincipal);
97
+
98
+ it('throws NotFoundException when organization does not exist (ensuring failure on non-existent organizations is handled correctly)', async () => {
99
+ organizationCollection.get.mockResolvedValue(null);
100
+
101
+ await expect(
102
+ service.deleteOrganization(organizationId, auditContext)
103
+ ).rejects.toThrow(NotFoundException);
104
+
105
+ expect(organizationCollection.get).toHaveBeenCalledWith(organizationId);
106
+ expect(organizationCollection.get).toHaveBeenCalledTimes(1);
107
+ expect(databaseService.withTransaction).not.toHaveBeenCalled();
108
+ });
109
+
110
+ it('successfully deletes organization with no roles (ensuring organizations without roles can be deleted)', async () => {
111
+ const organization = TestUtils.Entities.createOrganization({ _id: organizationId });
112
+ const mockSession = {} as ClientSession;
113
+
114
+ organizationCollection.get.mockResolvedValue(organization);
115
+ organizationRoleService.find.mockResolvedValue([]);
116
+ databaseService.withTransaction.mockImplementation(async ({ }, fn) => {
117
+ return await fn(mockSession);
118
+ });
119
+ organizationCollection.deleteOne.mockResolvedValue(undefined as any);
120
+
121
+ const result = await service.deleteOrganization(organizationId, auditContext);
122
+
123
+ expect(organizationCollection.get).toHaveBeenCalledWith(organizationId);
124
+ expect(organizationRoleService.find).toHaveBeenCalledWith(organizationId);
125
+ expect(databaseService.withTransaction).toHaveBeenCalledWith({}, expect.any(Function));
126
+ expect(organizationCollection.deleteOne).toHaveBeenCalledWith(organizationId, auditContext, mockSession);
127
+ expect(roleCollection.deleteOne).not.toHaveBeenCalled();
128
+ expect(result).toEqual({
129
+ organizationDeleted: true,
130
+ rolesDeletedCount: 0,
131
+ });
132
+ });
133
+
134
+ it('successfully deletes organization with multiple roles (ensuring cascade delete of roles works correctly)', async () => {
135
+ const organization = TestUtils.Entities.createOrganization({ _id: organizationId });
136
+ const role1 = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
137
+ const role2 = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
138
+ const role3 = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
139
+ const organizationRoles = [role1, role2, role3];
140
+ const mockSession = {} as ClientSession;
141
+
142
+ organizationCollection.get.mockResolvedValue(organization);
143
+ organizationRoleService.find.mockResolvedValue(organizationRoles);
144
+ databaseService.withTransaction.mockImplementation(async ({ }, fn) => {
145
+ return await fn(mockSession);
146
+ });
147
+ roleCollection.deleteOne.mockResolvedValue(undefined as any);
148
+ organizationCollection.deleteOne.mockResolvedValue(undefined as any);
149
+
150
+ const result = await service.deleteOrganization(organizationId, auditContext);
151
+
152
+ expect(organizationCollection.get).toHaveBeenCalledWith(organizationId);
153
+ expect(organizationRoleService.find).toHaveBeenCalledWith(organizationId);
154
+ expect(databaseService.withTransaction).toHaveBeenCalledWith({}, expect.any(Function));
155
+ expect(roleCollection.deleteOne).toHaveBeenCalledTimes(3);
156
+ expect(roleCollection.deleteOne).toHaveBeenCalledWith(role1._id, auditContext, mockSession);
157
+ expect(roleCollection.deleteOne).toHaveBeenCalledWith(role2._id, auditContext, mockSession);
158
+ expect(roleCollection.deleteOne).toHaveBeenCalledWith(role3._id, auditContext, mockSession);
159
+ expect(organizationCollection.deleteOne).toHaveBeenCalledWith(organizationId, auditContext, mockSession);
160
+ expect(result).toEqual({
161
+ organizationDeleted: true,
162
+ rolesDeletedCount: 3,
163
+ });
164
+ });
165
+
166
+ it('successfully deletes organization with single role (ensuring single role deletion works)', async () => {
167
+ const organization = TestUtils.Entities.createOrganization({ _id: organizationId });
168
+ const role = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
169
+ const organizationRoles = [role];
170
+ const mockSession = {} as ClientSession;
171
+
172
+ organizationCollection.get.mockResolvedValue(organization);
173
+ organizationRoleService.find.mockResolvedValue(organizationRoles);
174
+ databaseService.withTransaction.mockImplementation(async ({ }, fn) => {
175
+ return await fn(mockSession);
176
+ });
177
+ roleCollection.deleteOne.mockResolvedValue(undefined as any);
178
+ organizationCollection.deleteOne.mockResolvedValue(undefined as any);
179
+
180
+ const result = await service.deleteOrganization(organizationId, auditContext);
181
+
182
+ expect(organizationCollection.get).toHaveBeenCalledWith(organizationId);
183
+ expect(organizationRoleService.find).toHaveBeenCalledWith(organizationId);
184
+ expect(databaseService.withTransaction).toHaveBeenCalledWith({}, expect.any(Function));
185
+ expect(roleCollection.deleteOne).toHaveBeenCalledTimes(1);
186
+ expect(roleCollection.deleteOne).toHaveBeenCalledWith(role._id, auditContext, mockSession);
187
+ expect(organizationCollection.deleteOne).toHaveBeenCalledWith(organizationId, auditContext, mockSession);
188
+ expect(result).toEqual({
189
+ organizationDeleted: true,
190
+ rolesDeletedCount: 1,
191
+ });
192
+ });
193
+
194
+ it('calls organization delete hook when provided (ensuring hook is executed within transaction)', async () => {
195
+ const organization = TestUtils.Entities.createOrganization({ _id: organizationId });
196
+ const role = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
197
+ const organizationRoles = [role];
198
+ const mockSession = {} as ClientSession;
199
+ const mockHook: jest.Mocked<OrganizationDeleteHook> = {
200
+ onHook: jest.fn().mockResolvedValue(undefined),
201
+ };
202
+
203
+ // Create a new module with the hook provider
204
+ const moduleWithHook: TestingModule = await Test.createTestingModule({
205
+ providers: [
206
+ OrganizationService,
207
+ {
208
+ provide: OrganizationCollection,
209
+ useValue: organizationCollection,
210
+ },
211
+ {
212
+ provide: RoleCollection,
213
+ useValue: roleCollection,
214
+ },
215
+ {
216
+ provide: OrganizationRoleService,
217
+ useValue: organizationRoleService,
218
+ },
219
+ {
220
+ provide: DatabaseService,
221
+ useValue: databaseService,
222
+ },
223
+ {
224
+ provide: PROVIDER_SYMBOLS.ORGANIZATION_DELETE_HOOK,
225
+ useValue: mockHook,
226
+ },
227
+ ],
228
+ }).compile();
229
+
230
+ const serviceWithHook = moduleWithHook.get<OrganizationService>(OrganizationService);
231
+
232
+ organizationCollection.get.mockResolvedValue(organization);
233
+ organizationRoleService.find.mockResolvedValue(organizationRoles);
234
+ databaseService.withTransaction.mockImplementation(async ({ }, fn) => {
235
+ return await fn(mockSession);
236
+ });
237
+ roleCollection.deleteOne.mockResolvedValue(undefined as any);
238
+ organizationCollection.deleteOne.mockResolvedValue(undefined as any);
239
+
240
+ const result = await serviceWithHook.deleteOrganization(organizationId, auditContext);
241
+
242
+ expect(organizationCollection.get).toHaveBeenCalledWith(organizationId);
243
+ expect(organizationRoleService.find).toHaveBeenCalledWith(organizationId);
244
+ expect(databaseService.withTransaction).toHaveBeenCalledWith({}, expect.any(Function));
245
+ expect(roleCollection.deleteOne).toHaveBeenCalledWith(role._id, auditContext, mockSession);
246
+ expect(mockHook.onHook).toHaveBeenCalledWith(organizationId, organization, auditContext, mockSession);
247
+ expect(organizationCollection.deleteOne).toHaveBeenCalledWith(organizationId, auditContext, mockSession);
248
+ expect(result).toEqual({
249
+ organizationDeleted: true,
250
+ rolesDeletedCount: 1,
251
+ });
252
+ });
253
+
254
+ it('does not call organization delete hook when not provided (ensuring hook is optional)', async () => {
255
+ const organization = TestUtils.Entities.createOrganization({ _id: organizationId });
256
+ const role = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
257
+ const organizationRoles = [role];
258
+ const mockSession = {} as ClientSession;
259
+
260
+ organizationCollection.get.mockResolvedValue(organization);
261
+ organizationRoleService.find.mockResolvedValue(organizationRoles);
262
+ databaseService.withTransaction.mockImplementation(async ({ }, fn) => {
263
+ return await fn(mockSession);
264
+ });
265
+ roleCollection.deleteOne.mockResolvedValue(undefined as any);
266
+ organizationCollection.deleteOne.mockResolvedValue(undefined as any);
267
+
268
+ const result = await service.deleteOrganization(organizationId, auditContext);
269
+
270
+ expect(organizationCollection.get).toHaveBeenCalledWith(organizationId);
271
+ expect(organizationRoleService.find).toHaveBeenCalledWith(organizationId);
272
+ expect(databaseService.withTransaction).toHaveBeenCalledWith({}, expect.any(Function));
273
+ expect(roleCollection.deleteOne).toHaveBeenCalledWith(role._id, auditContext, mockSession);
274
+ expect(organizationCollection.deleteOne).toHaveBeenCalledWith(organizationId, auditContext, mockSession);
275
+ expect(result).toEqual({
276
+ organizationDeleted: true,
277
+ rolesDeletedCount: 1,
278
+ });
279
+ });
280
+
281
+ it('executes all operations within a single transaction (ensuring atomicity)', async () => {
282
+ const organization = TestUtils.Entities.createOrganization({ _id: organizationId });
283
+ const role1 = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
284
+ const role2 = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
285
+ const organizationRoles = [role1, role2];
286
+ const mockSession = {} as ClientSession;
287
+ let transactionCallback: ((session: ClientSession) => Promise<any>) | null = null;
288
+
289
+ organizationCollection.get.mockResolvedValue(organization);
290
+ organizationRoleService.find.mockResolvedValue(organizationRoles);
291
+ databaseService.withTransaction.mockImplementation(async ({ }, fn) => {
292
+ transactionCallback = fn;
293
+ return await fn(mockSession);
294
+ });
295
+ roleCollection.deleteOne.mockResolvedValue(undefined as any);
296
+ organizationCollection.deleteOne.mockResolvedValue(undefined as any);
297
+
298
+ await service.deleteOrganization(organizationId, auditContext);
299
+
300
+ // Verify that withTransaction was called
301
+ expect(databaseService.withTransaction).toHaveBeenCalledWith({}, expect.any(Function));
302
+
303
+ // Verify that all delete operations were called with the same session
304
+ expect(roleCollection.deleteOne).toHaveBeenCalledWith(role1._id, auditContext, mockSession);
305
+ expect(roleCollection.deleteOne).toHaveBeenCalledWith(role2._id, auditContext, mockSession);
306
+ expect(organizationCollection.deleteOne).toHaveBeenCalledWith(organizationId, auditContext, mockSession);
307
+ });
308
+
309
+ it('handles transaction errors properly (ensuring transaction rollback on error)', async () => {
310
+ const organization = TestUtils.Entities.createOrganization({ _id: organizationId });
311
+ const role = TestUtils.Models.createOrganizationRoleModel({ organization_id: organizationId });
312
+ const organizationRoles = [role];
313
+ const mockSession = {} as ClientSession;
314
+ const transactionError = new Error('Transaction failed');
315
+
316
+ organizationCollection.get.mockResolvedValue(organization);
317
+ organizationRoleService.find.mockResolvedValue(organizationRoles);
318
+ databaseService.withTransaction.mockImplementation(async ({ }, fn) => {
319
+ throw transactionError;
320
+ });
321
+
322
+ await expect(
323
+ service.deleteOrganization(organizationId, auditContext)
324
+ ).rejects.toThrow('Transaction failed');
325
+
326
+ expect(organizationCollection.get).toHaveBeenCalledWith(organizationId);
327
+ expect(organizationRoleService.find).toHaveBeenCalledWith(organizationId);
328
+ expect(databaseService.withTransaction).toHaveBeenCalledWith({}, expect.any(Function));
329
+ // Verify that delete operations were attempted but transaction failed
330
+ expect(roleCollection.deleteOne).not.toHaveBeenCalled();
331
+ expect(organizationCollection.deleteOne).not.toHaveBeenCalled();
332
+ });
333
+
334
+ it('handles hook errors within transaction (ensuring hook errors propagate)', async () => {
335
+ const organization = TestUtils.Entities.createOrganization({ _id: organizationId });
336
+ const mockSession = {} as ClientSession;
337
+ const hookError = new Error('Hook execution failed');
338
+ const mockHook: jest.Mocked<OrganizationDeleteHook> = {
339
+ onHook: jest.fn().mockRejectedValue(hookError),
340
+ };
341
+
342
+ // Create a new module with the hook provider
343
+ const moduleWithHook: TestingModule = await Test.createTestingModule({
344
+ providers: [
345
+ OrganizationService,
346
+ {
347
+ provide: OrganizationCollection,
348
+ useValue: organizationCollection,
349
+ },
350
+ {
351
+ provide: RoleCollection,
352
+ useValue: roleCollection,
353
+ },
354
+ {
355
+ provide: OrganizationRoleService,
356
+ useValue: organizationRoleService,
357
+ },
358
+ {
359
+ provide: DatabaseService,
360
+ useValue: databaseService,
361
+ },
362
+ {
363
+ provide: PROVIDER_SYMBOLS.ORGANIZATION_DELETE_HOOK,
364
+ useValue: mockHook,
365
+ },
366
+ ],
367
+ }).compile();
368
+
369
+ const serviceWithHook = moduleWithHook.get<OrganizationService>(OrganizationService);
370
+
371
+ organizationCollection.get.mockResolvedValue(organization);
372
+ organizationRoleService.find.mockResolvedValue([]);
373
+ databaseService.withTransaction.mockImplementation(async ({ }, fn) => {
374
+ return await fn(mockSession);
375
+ });
376
+
377
+ await expect(
378
+ serviceWithHook.deleteOrganization(organizationId, auditContext)
379
+ ).rejects.toThrow('Hook execution failed');
380
+
381
+ expect(mockHook.onHook).toHaveBeenCalledWith(organizationId, organization, auditContext, mockSession);
382
+ expect(organizationCollection.deleteOne).not.toHaveBeenCalled();
383
+ });
384
+ });
385
+ });
@@ -0,0 +1,114 @@
1
+ import { AccountCollection } from '@/collections/account.collection';
2
+ import { AccountService } from '@/services/account.service';
3
+ import { SecurityAuditCollection } from '@/collections/security-audit.collection';
4
+ import { ApiKeyCollection } from '@/collections/api-key.collection';
5
+ import { AuthService } from '@/services/auth.service';
6
+ import { AuthGuardHook } from '@/types';
7
+ import { PROVIDER_SYMBOLS } from '@/constants';
8
+
9
+ export interface AuthGuardMocks {
10
+ accountCollection: jest.Mocked<AccountCollection>;
11
+ accountService: jest.Mocked<AccountService>;
12
+ securityAuditCollection: jest.Mocked<SecurityAuditCollection>;
13
+ apiKeyCollection: jest.Mocked<ApiKeyCollection>;
14
+ authService: jest.Mocked<AuthService>;
15
+ authGuardHook?: jest.Mocked<AuthGuardHook>;
16
+ }
17
+
18
+ export interface AuthGuardProviders {
19
+ mocks: AuthGuardMocks;
20
+ providers: any[];
21
+ }
22
+
23
+ /**
24
+ * Creates mock dependencies and providers for AuthGuard testing.
25
+ *
26
+ * @param overrides Optional overrides for default mock implementations
27
+ * @returns Object containing mocks and providers array for NestJS TestingModule
28
+ */
29
+ export function createAuthGuardDependencies(overrides?: {
30
+ accountCollection?: Partial<jest.Mocked<AccountCollection>>;
31
+ accountService?: Partial<jest.Mocked<AccountService>>;
32
+ securityAuditCollection?: Partial<jest.Mocked<SecurityAuditCollection>>;
33
+ apiKeyCollection?: Partial<jest.Mocked<ApiKeyCollection>>;
34
+ authService?: Partial<jest.Mocked<AuthService>>;
35
+ authGuardHook?: Partial<jest.Mocked<AuthGuardHook>>;
36
+ }): AuthGuardProviders {
37
+ const mockAccountCollection: jest.Mocked<AccountCollection> = {
38
+ findOne: jest.fn(),
39
+ upsertOne: jest.fn(),
40
+ ...overrides?.accountCollection,
41
+ } as any;
42
+
43
+ const mockAccountService: jest.Mocked<AccountService> = {
44
+ getAccount: jest.fn(),
45
+ upsertAccount: jest.fn(),
46
+ ...overrides?.accountService,
47
+ } as any;
48
+
49
+ const mockSecurityAuditCollection: jest.Mocked<SecurityAuditCollection> = {
50
+ insertOne: jest.fn(),
51
+ ...overrides?.securityAuditCollection,
52
+ } as any;
53
+
54
+ const mockApiKeyCollection: jest.Mocked<ApiKeyCollection> = {
55
+ findOne: jest.fn(),
56
+ ...overrides?.apiKeyCollection,
57
+ } as any;
58
+
59
+ const mockAuthService: jest.Mocked<AuthService> = {
60
+ getPrincipal: jest.fn(),
61
+ validateAuth: jest.fn(),
62
+ ...overrides?.authService,
63
+ } as any;
64
+
65
+ const mockAuthGuardHook: jest.Mocked<AuthGuardHook> | undefined = overrides?.authGuardHook
66
+ ? {
67
+ onHook: jest.fn(),
68
+ ...overrides.authGuardHook,
69
+ } as any
70
+ : undefined;
71
+
72
+ const providers = [
73
+ {
74
+ provide: AccountService,
75
+ useValue: mockAccountService,
76
+ },
77
+ {
78
+ provide: AccountCollection,
79
+ useValue: mockAccountCollection,
80
+ },
81
+ {
82
+ provide: SecurityAuditCollection,
83
+ useValue: mockSecurityAuditCollection,
84
+ },
85
+ {
86
+ provide: ApiKeyCollection,
87
+ useValue: mockApiKeyCollection,
88
+ },
89
+ {
90
+ provide: AuthService,
91
+ useValue: mockAuthService,
92
+ },
93
+ ...(mockAuthGuardHook
94
+ ? [
95
+ {
96
+ provide: PROVIDER_SYMBOLS.AUTH_GUARD_HOOK,
97
+ useValue: mockAuthGuardHook,
98
+ },
99
+ ]
100
+ : []),
101
+ ];
102
+
103
+ return {
104
+ mocks: {
105
+ accountCollection: mockAccountCollection,
106
+ accountService: mockAccountService,
107
+ securityAuditCollection: mockSecurityAuditCollection,
108
+ apiKeyCollection: mockApiKeyCollection,
109
+ authService: mockAuthService,
110
+ ...(mockAuthGuardHook ? { authGuardHook: mockAuthGuardHook } : {}),
111
+ },
112
+ providers,
113
+ };
114
+ }
@@ -0,0 +1,16 @@
1
+ import { BaseEntity, baseEntitySchema } from '@trailmix-cms/models';
2
+ import { faker } from '@faker-js/faker';
3
+ import dayjs from 'dayjs';
4
+ import { ObjectId } from 'mongodb';
5
+
6
+ export function createBaseEntity() {
7
+ const createdAt = dayjs(faker.date.past()).toDate();
8
+ const updatedAt = dayjs(createdAt).add(1, 'day').toDate();
9
+ const entity: BaseEntity = {
10
+ _id: new ObjectId(),
11
+ created_at: createdAt,
12
+ updated_at: updatedAt,
13
+ };
14
+ baseEntitySchema.encode(entity);
15
+ return entity;
16
+ }
@@ -0,0 +1,13 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { Account } from '@trailmix-cms/models';
3
+ import { createBaseEntity } from '../base';
4
+
5
+ export function createAccount(overrides?: Partial<Account.Entity>) {
6
+ const entity: Account.Entity = {
7
+ ...createBaseEntity(),
8
+ user_id: faker.string.uuid(),
9
+ ...overrides,
10
+ };
11
+ Account.schema.encode(entity);
12
+ return entity;
13
+ }
@@ -0,0 +1,15 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { ApiKey, ApiKeyScope } from '@trailmix-cms/models';
3
+ import { createBaseEntity } from '../base';
4
+
5
+ export function createApiKey(overrides?: Partial<ApiKey.Entity>) {
6
+ const entity: ApiKey.Entity = {
7
+ ...createBaseEntity(),
8
+ api_key: faker.string.alphanumeric(32),
9
+ name: faker.word.noun(),
10
+ scope_type: ApiKeyScope.Global,
11
+ ...overrides,
12
+ };
13
+ ApiKey.schema.encode(entity);
14
+ return entity;
15
+ }
@@ -0,0 +1,18 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { Audit } from '@trailmix-cms/models';
3
+ import { ObjectId } from 'mongodb';
4
+ import { createBaseEntity } from '../base';
5
+ import { createAuditContext } from '../models/audit-context';
6
+
7
+ export function createAudit(overrides?: Partial<Audit.Entity>) {
8
+ const entity: Audit.Entity = {
9
+ ...createBaseEntity(),
10
+ entity_id: new ObjectId(),
11
+ entity_type: faker.string.alpha(10),
12
+ action: faker.helpers.arrayElement(['create', 'update', 'delete'] as const),
13
+ context: createAuditContext(),
14
+ ...overrides,
15
+ };
16
+ Audit.schema.encode(entity);
17
+ return entity;
18
+ }
@@ -0,0 +1,6 @@
1
+ export * from './account';
2
+ export * from './api-key';
3
+ export * from './audit';
4
+ export * from './organization';
5
+ export * from './security-audit';
6
+ export * from './role';
@@ -0,0 +1,20 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { Mapping } from '@trailmix-cms/models';
3
+ import { ObjectId } from 'mongodb';
4
+ import { createBaseEntity } from '../base';
5
+
6
+ export function createMapping(overrides?: Partial<Mapping.Entity>) {
7
+ const firstCollection = faker.helpers.arrayElement(['account', 'organization', 'file']);
8
+ const secondCollection = faker.helpers.arrayElement(['text', 'file', 'organization']);
9
+
10
+ const entity: Mapping.Entity = {
11
+ ...createBaseEntity(),
12
+ first_collection: firstCollection,
13
+ first_id: new ObjectId(),
14
+ second_collection: secondCollection,
15
+ second_id: new ObjectId(),
16
+ ...overrides,
17
+ };
18
+ Mapping.schema.encode(entity);
19
+ return entity;
20
+ }
@@ -0,0 +1,13 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { Organization } from '@trailmix-cms/models';
3
+ import { createBaseEntity } from '../base';
4
+
5
+ export function createOrganization(overrides?: Partial<Organization.Entity>) {
6
+ const entity: Organization.Entity = {
7
+ ...createBaseEntity(),
8
+ name: faker.company.name(),
9
+ ...overrides,
10
+ };
11
+ Organization.schema.encode(entity);
12
+ return entity;
13
+ }
@@ -0,0 +1,21 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { Role } from '@trailmix-cms/models';
3
+ import { createBaseEntity } from '../base';
4
+ import { RoleType } from '@trailmix-cms/models';
5
+ import { ObjectId } from 'mongodb';
6
+ import { Principal } from '@trailmix-cms/models';
7
+ import { RoleValue } from '@trailmix-cms/models';
8
+
9
+ export function createRole(overrides?: Partial<Role.Entity>) {
10
+ const entity: Role.Entity = {
11
+ ...createBaseEntity(),
12
+ type: faker.helpers.arrayElement(Object.values(RoleType)),
13
+ principal_id: new ObjectId(),
14
+ principal_type: faker.helpers.arrayElement(Object.values(Principal)),
15
+ organization_id: new ObjectId(),
16
+ role: faker.helpers.arrayElement(Object.values(RoleValue)),
17
+ ...overrides,
18
+ };
19
+ Role.schema.encode(entity);
20
+ return entity;
21
+ }
@@ -0,0 +1,16 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import { SecurityAudit, Principal, SecurityAuditEventType } from '@trailmix-cms/models';
3
+ import { ObjectId } from 'mongodb';
4
+ import { createBaseEntity } from '../base';
5
+
6
+ export function createSecurityAudit(overrides?: Partial<SecurityAudit.Entity>) {
7
+ const entity: SecurityAudit.Entity = {
8
+ ...createBaseEntity(),
9
+ event_type: faker.helpers.arrayElement(Object.values(SecurityAuditEventType)),
10
+ principal_id: new ObjectId(),
11
+ principal_type: faker.helpers.arrayElement(Object.values(Principal)),
12
+ ...overrides,
13
+ };
14
+ SecurityAudit.schema.encode(entity);
15
+ return entity;
16
+ }
@@ -0,0 +1,4 @@
1
+ export * as Entities from './entities';
2
+ export * as Models from './models';
3
+ export * as Base from './base';
4
+ export * from './auth-guard';
@@ -0,0 +1,10 @@
1
+ import { AuditContext } from '@trailmix-cms/models';
2
+
3
+ export function createAuditContext(overrides?: Partial<AuditContext.Model>) {
4
+ const model: AuditContext.Model = {
5
+ system: true,
6
+ ...overrides,
7
+ };
8
+ AuditContext.schema.encode(model);
9
+ return model;
10
+ }
@@ -0,0 +1,7 @@
1
+ import { Authorization } from '@trailmix-cms/models';
2
+
3
+ export function createAuthorization(overrides?: Partial<Authorization.Model>) {
4
+ return Authorization.schema.parse({
5
+ ...overrides,
6
+ });
7
+ }