UserMembershipHandler.test.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. import { vi, expect } from 'vitest'
  2. import mongodb from 'mongodb-legacy'
  3. import EntityConfigs from '../../../../app/src/Features/UserMembership/UserMembershipEntityConfigs.mjs'
  4. import UserMembershipErrors from '../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs'
  5. const { ObjectId } = mongodb
  6. const modulePath =
  7. '../../../../app/src/Features/UserMembership/UserMembershipHandler'
  8. const serializeIds = ids =>
  9. ids.map(id => (id instanceof ObjectId ? `objectId-${id.toString()}` : id))
  10. vi.mock(
  11. '../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs',
  12. () =>
  13. vi.importActual(
  14. '../../../../app/src/Features/UserMembership/UserMembershipErrors.mjs'
  15. )
  16. )
  17. describe('UserMembershipHandler', function () {
  18. beforeEach(async function (ctx) {
  19. ctx.user = { _id: new ObjectId() }
  20. ctx.newUser = { _id: new ObjectId(), email: 'new-user-email@foo.bar' }
  21. ctx.fakeEntityId = new ObjectId()
  22. ctx.subscription = {
  23. _id: 'mock-subscription-id',
  24. groupPlan: true,
  25. membersLimit: 10,
  26. member_ids: [new ObjectId(), new ObjectId()],
  27. manager_ids: [new ObjectId()],
  28. invited_emails: ['mock-email-1@foo.com'],
  29. teamInvites: [{ email: 'mock-email-1@bar.com' }],
  30. update: vi.fn().mockReturnValue({
  31. exec: vi.fn().mockResolvedValue(),
  32. }),
  33. updateOne: vi.fn().mockReturnValue({
  34. exec: vi.fn().mockResolvedValue(),
  35. }),
  36. }
  37. ctx.institution = {
  38. _id: 'mock-institution-id',
  39. v1Id: 123,
  40. managerIds: [new ObjectId(), new ObjectId(), new ObjectId()],
  41. updateOne: vi.fn().mockReturnValue({
  42. exec: vi.fn().mockResolvedValue(),
  43. }),
  44. }
  45. ctx.publisher = {
  46. _id: 'mock-publisher-id',
  47. slug: 'slug',
  48. managerIds: [new ObjectId(), new ObjectId()],
  49. updateOne: vi.fn().mockReturnValue({
  50. exec: vi.fn().mockResolvedValue(),
  51. }),
  52. }
  53. ctx.UserMembershipViewModel = {
  54. promises: {
  55. buildAsync: vi.fn().mockResolvedValue([{ _id: 'mock-member-id' }]),
  56. },
  57. build: vi.fn().mockReturnValue(ctx.newUser),
  58. }
  59. ctx.UserGetter = {
  60. promises: {
  61. getUserByAnyEmail: vi.fn().mockResolvedValue(ctx.newUser),
  62. },
  63. }
  64. ctx.Institution = {
  65. findOne: vi.fn().mockReturnValue({
  66. exec: vi.fn().mockResolvedValue(ctx.institution),
  67. }),
  68. }
  69. ctx.Subscription = {
  70. findOne: vi.fn().mockReturnValue({
  71. exec: vi.fn().mockResolvedValue(ctx.subscription),
  72. }),
  73. }
  74. ctx.Publisher = {
  75. findOne: vi.fn().mockReturnValue({
  76. exec: vi.fn().mockResolvedValue(ctx.publisher),
  77. }),
  78. create: vi.fn().mockReturnValue({
  79. exec: vi.fn().mockResolvedValue(ctx.publisher),
  80. }),
  81. }
  82. ctx.Modules = {
  83. promises: {
  84. hooks: {
  85. fire: vi.fn().mockResolvedValue(),
  86. },
  87. },
  88. }
  89. ctx.mongoose = {
  90. startSession: vi.fn().mockResolvedValue({
  91. withTransaction: vi.fn(async callback => await callback()),
  92. endSession: vi.fn().mockResolvedValue(),
  93. }),
  94. }
  95. ctx.SubscriptionUpdater = {
  96. promises: {
  97. sendGroupRoleUserProperty: vi.fn().mockResolvedValue(),
  98. },
  99. }
  100. vi.doMock('mongodb-legacy', () => ({
  101. default: { ObjectId },
  102. }))
  103. vi.doMock(
  104. '../../../../app/src/Features/UserMembership/UserMembershipViewModel',
  105. () => ({
  106. default: ctx.UserMembershipViewModel,
  107. })
  108. )
  109. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  110. default: ctx.UserGetter,
  111. }))
  112. vi.doMock('../../../../app/src/models/Institution', () => ({
  113. Institution: ctx.Institution,
  114. }))
  115. vi.doMock('../../../../app/src/models/Subscription', () => ({
  116. Subscription: ctx.Subscription,
  117. }))
  118. vi.doMock('../../../../app/src/models/Publisher', () => ({
  119. Publisher: ctx.Publisher,
  120. }))
  121. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  122. default: ctx.Modules,
  123. }))
  124. vi.doMock('../../../../app/src/infrastructure/Mongoose', () => ({
  125. default: ctx.mongoose,
  126. }))
  127. vi.doMock(
  128. '../../../../app/src/Features/Subscription/SubscriptionUpdater',
  129. () => ({
  130. default: ctx.SubscriptionUpdater,
  131. })
  132. )
  133. ctx.UserMembershipHandler = (await import(modulePath)).default
  134. })
  135. describe('getEntityWithoutAuthorizationCheck', function () {
  136. it('get publisher', async function (ctx) {
  137. const subscription =
  138. await ctx.UserMembershipHandler.promises.getEntityWithoutAuthorizationCheck(
  139. ctx.fakeEntityId,
  140. EntityConfigs.publisher
  141. )
  142. const expectedQuery = { slug: ctx.fakeEntityId }
  143. expect(ctx.Publisher.findOne).toHaveBeenCalledWith(expectedQuery)
  144. expect(subscription).to.equal(ctx.publisher)
  145. })
  146. })
  147. describe('getUsers', function () {
  148. describe('group', function () {
  149. it('build view model for all users', async function (ctx) {
  150. await ctx.UserMembershipHandler.promises.getUsers(
  151. ctx.subscription,
  152. EntityConfigs.group
  153. )
  154. expect(
  155. serializeIds(
  156. ctx.UserMembershipViewModel.promises.buildAsync.mock.calls[0][0]
  157. )
  158. ).toEqual(
  159. serializeIds(
  160. ctx.subscription.invited_emails.concat(
  161. ctx.subscription.teamInvites[0].email,
  162. ctx.subscription.member_ids
  163. )
  164. )
  165. )
  166. })
  167. })
  168. describe('group managers', function () {
  169. it('build view model for all managers', async function (ctx) {
  170. await ctx.UserMembershipHandler.promises.getUsers(
  171. ctx.subscription,
  172. EntityConfigs.groupManagers
  173. )
  174. expect(
  175. serializeIds(
  176. ctx.UserMembershipViewModel.promises.buildAsync.mock.calls[0][0]
  177. )
  178. ).toEqual(serializeIds(ctx.subscription.manager_ids))
  179. })
  180. })
  181. describe('institution', function () {
  182. it('build view model for all managers', async function (ctx) {
  183. await ctx.UserMembershipHandler.promises.getUsers(
  184. ctx.institution,
  185. EntityConfigs.institution
  186. )
  187. expect(
  188. serializeIds(
  189. ctx.UserMembershipViewModel.promises.buildAsync.mock.calls[0][0]
  190. )
  191. ).toEqual(serializeIds(ctx.institution.managerIds))
  192. })
  193. })
  194. })
  195. describe('createEntity', function () {
  196. it('creates publisher', async function (ctx) {
  197. await ctx.UserMembershipHandler.promises.createEntity(
  198. ctx.fakeEntityId,
  199. EntityConfigs.publisher
  200. )
  201. expect(ctx.Publisher.create).toHaveBeenCalledWith({
  202. slug: ctx.fakeEntityId,
  203. })
  204. })
  205. })
  206. describe('addUser', function () {
  207. beforeEach(function (ctx) {
  208. ctx.email = ctx.newUser.email
  209. })
  210. describe('institution', function () {
  211. it('get user', async function (ctx) {
  212. await ctx.UserMembershipHandler.promises.addUser(
  213. ctx.institution,
  214. EntityConfigs.institution,
  215. ctx.email
  216. )
  217. expect(ctx.UserGetter.promises.getUserByAnyEmail).toHaveBeenCalledWith(
  218. ctx.email
  219. )
  220. })
  221. it('handle user not found', async function (ctx) {
  222. ctx.UserGetter.promises.getUserByAnyEmail.mockResolvedValue(null)
  223. try {
  224. await ctx.UserMembershipHandler.promises.addUser(
  225. ctx.institution,
  226. EntityConfigs.institution,
  227. ctx.email
  228. )
  229. expect.fail('Expected addUser to throw')
  230. } catch (err) {
  231. expect(err).toBeInstanceOf(UserMembershipErrors.UserNotFoundError)
  232. }
  233. })
  234. it('handle user already added', async function (ctx) {
  235. ctx.institution.managerIds.push(ctx.newUser._id)
  236. try {
  237. await ctx.UserMembershipHandler.promises.addUser(
  238. ctx.institution,
  239. EntityConfigs.institution,
  240. ctx.email
  241. )
  242. expect.fail('Expected addUser to throw')
  243. } catch (err) {
  244. expect(err).toBeInstanceOf(UserMembershipErrors.UserAlreadyAddedError)
  245. }
  246. })
  247. it('add user to institution', async function (ctx) {
  248. await ctx.UserMembershipHandler.promises.addUser(
  249. ctx.institution,
  250. EntityConfigs.institution,
  251. ctx.email
  252. )
  253. expect(ctx.institution.updateOne).toHaveBeenCalledWith({
  254. $addToSet: { managerIds: ctx.newUser._id },
  255. })
  256. })
  257. it('return user view', async function (ctx) {
  258. const user = await ctx.UserMembershipHandler.promises.addUser(
  259. ctx.institution,
  260. EntityConfigs.institution,
  261. ctx.email
  262. )
  263. expect(user).to.equal(ctx.newUser)
  264. })
  265. })
  266. describe('group managers', function () {
  267. it('add user to group managers', async function (ctx) {
  268. await ctx.UserMembershipHandler.promises.addUser(
  269. ctx.subscription,
  270. EntityConfigs.groupManagers,
  271. ctx.email
  272. )
  273. expect(ctx.subscription.updateOne).toHaveBeenCalledWith({
  274. $addToSet: { manager_ids: ctx.newUser._id },
  275. })
  276. })
  277. it('should write a group audit log when subscription has managed users enabled', async function (ctx) {
  278. ctx.subscription.managedUsersEnabled = true
  279. const auditInfo = {
  280. initiatorId: new ObjectId(),
  281. ipAddress: '192.168.1.1',
  282. }
  283. await ctx.UserMembershipHandler.promises.addUser(
  284. ctx.subscription,
  285. EntityConfigs.groupManagers,
  286. ctx.email,
  287. auditInfo
  288. )
  289. expect(ctx.Modules.promises.hooks.fire).toHaveBeenCalledWith(
  290. 'addGroupAuditLogEntry',
  291. {
  292. groupId: ctx.subscription._id,
  293. operation: 'group-role-changed',
  294. initiatorId: auditInfo.initiatorId,
  295. ipAddress: auditInfo.ipAddress,
  296. info: {
  297. userId: ctx.newUser._id,
  298. role: 'manager',
  299. },
  300. },
  301. expect.anything() // session object
  302. )
  303. })
  304. it('should not write a group audit log when subscription does not have managed users enabled', async function (ctx) {
  305. ctx.subscription.managedUsersEnabled = false
  306. const auditInfo = {
  307. initiatorId: new ObjectId(),
  308. ipAddress: '192.168.1.1',
  309. }
  310. await ctx.UserMembershipHandler.promises.addUser(
  311. ctx.subscription,
  312. EntityConfigs.groupManagers,
  313. ctx.email,
  314. auditInfo
  315. )
  316. expect(ctx.Modules.promises.hooks.fire).not.toHaveBeenCalled()
  317. })
  318. })
  319. })
  320. describe('removeUser', function () {
  321. describe('institution', function () {
  322. it('remove user from institution', async function (ctx) {
  323. await ctx.UserMembershipHandler.promises.removeUser(
  324. ctx.institution,
  325. EntityConfigs.institution,
  326. ctx.newUser._id
  327. )
  328. expect(ctx.institution.updateOne).toHaveBeenCalledWith({
  329. $pull: { managerIds: ctx.newUser._id },
  330. })
  331. })
  332. it('handle admin', async function (ctx) {
  333. ctx.subscription.admin_id = ctx.newUser._id
  334. try {
  335. await ctx.UserMembershipHandler.promises.removeUser(
  336. ctx.subscription,
  337. EntityConfigs.groupManagers,
  338. ctx.newUser._id
  339. )
  340. expect.fail('Expected removeUser to throw')
  341. } catch (err) {
  342. expect(err).toBeInstanceOf(UserMembershipErrors.UserIsManagerError)
  343. }
  344. })
  345. })
  346. describe('group managers', function () {
  347. it('remove user from group managers', async function (ctx) {
  348. await ctx.UserMembershipHandler.promises.removeUser(
  349. ctx.subscription,
  350. EntityConfigs.groupManagers,
  351. ctx.newUser._id
  352. )
  353. expect(ctx.subscription.updateOne).toHaveBeenCalledWith({
  354. $pull: { manager_ids: ctx.newUser._id },
  355. })
  356. })
  357. it('should write a group audit log when subscription has managed users enabled', async function (ctx) {
  358. ctx.subscription.managedUsersEnabled = true
  359. const auditInfo = {
  360. initiatorId: new ObjectId(),
  361. ipAddress: '192.168.1.1',
  362. }
  363. await ctx.UserMembershipHandler.promises.removeUser(
  364. ctx.subscription,
  365. EntityConfigs.groupManagers,
  366. ctx.newUser._id,
  367. auditInfo
  368. )
  369. expect(ctx.Modules.promises.hooks.fire).toHaveBeenCalledWith(
  370. 'addGroupAuditLogEntry',
  371. {
  372. groupId: ctx.subscription._id,
  373. operation: 'group-role-changed',
  374. initiatorId: auditInfo.initiatorId,
  375. ipAddress: auditInfo.ipAddress,
  376. info: {
  377. userId: ctx.newUser._id,
  378. role: 'member',
  379. },
  380. }
  381. )
  382. })
  383. it('should not write a group audit log when subscription does not have managed users enabled', async function (ctx) {
  384. ctx.subscription.managedUsersEnabled = false
  385. const auditInfo = {
  386. initiatorId: new ObjectId(),
  387. ipAddress: '192.168.1.1',
  388. }
  389. await ctx.UserMembershipHandler.promises.removeUser(
  390. ctx.subscription,
  391. EntityConfigs.groupManagers,
  392. ctx.newUser._id,
  393. auditInfo
  394. )
  395. expect(ctx.Modules.promises.hooks.fire).not.toHaveBeenCalled()
  396. })
  397. })
  398. })
  399. })