OwnershipTransferHandlerTests.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. const SandboxedModule = require('sandboxed-module')
  2. const sinon = require('sinon')
  3. const { expect } = require('chai')
  4. const PrivilegeLevels = require('../../../../app/src/Features/Authorization/PrivilegeLevels')
  5. const Errors = require('../../../../app/src/Features/Errors/Errors')
  6. const { ObjectId } = require('mongodb')
  7. const MODULE_PATH =
  8. '../../../../app/src/Features/Collaborators/OwnershipTransferHandler'
  9. describe('OwnershipTransferHandler', function () {
  10. beforeEach(function () {
  11. this.user = { _id: ObjectId(), email: 'owner@example.com' }
  12. this.collaborator = { _id: ObjectId(), email: 'collaborator@example.com' }
  13. this.project = {
  14. _id: ObjectId(),
  15. name: 'project',
  16. owner_ref: this.user._id,
  17. collaberator_refs: [this.collaborator._id],
  18. }
  19. this.ProjectGetter = {
  20. promises: {
  21. getProject: sinon.stub().resolves(this.project),
  22. },
  23. }
  24. this.ProjectModel = {
  25. updateOne: sinon.stub().returns({
  26. exec: sinon.stub().resolves(),
  27. }),
  28. }
  29. this.UserGetter = {
  30. promises: {
  31. getUser: sinon.stub().resolves(this.user),
  32. },
  33. }
  34. this.TpdsUpdateSender = {
  35. promises: {
  36. moveEntity: sinon.stub().resolves(),
  37. },
  38. }
  39. this.TpdsProjectFlusher = {
  40. promises: {
  41. flushProjectToTpds: sinon.stub().resolves(),
  42. },
  43. }
  44. this.CollaboratorsHandler = {
  45. promises: {
  46. removeUserFromProject: sinon.stub().resolves(),
  47. addUserIdToProject: sinon.stub().resolves(),
  48. },
  49. }
  50. this.EmailHandler = {
  51. promises: {
  52. sendEmail: sinon.stub().resolves(),
  53. },
  54. }
  55. this.ProjectAuditLogHandler = {
  56. promises: {
  57. addEntry: sinon.stub().resolves(),
  58. },
  59. }
  60. this.handler = SandboxedModule.require(MODULE_PATH, {
  61. requires: {
  62. '../Project/ProjectGetter': this.ProjectGetter,
  63. '../../models/Project': {
  64. Project: this.ProjectModel,
  65. },
  66. '../User/UserGetter': this.UserGetter,
  67. '../ThirdPartyDataStore/TpdsProjectFlusher': this.TpdsProjectFlusher,
  68. '../Project/ProjectAuditLogHandler': this.ProjectAuditLogHandler,
  69. '../Email/EmailHandler': this.EmailHandler,
  70. './CollaboratorsHandler': this.CollaboratorsHandler,
  71. },
  72. })
  73. })
  74. describe('transferOwnership', function () {
  75. beforeEach(function () {
  76. this.UserGetter.promises.getUser
  77. .withArgs(this.user._id)
  78. .resolves(this.user)
  79. this.UserGetter.promises.getUser
  80. .withArgs(this.collaborator._id)
  81. .resolves(this.collaborator)
  82. })
  83. it("should return a not found error if the project can't be found", async function () {
  84. this.ProjectGetter.promises.getProject.resolves(null)
  85. await expect(
  86. this.handler.promises.transferOwnership('abc', this.collaborator._id)
  87. ).to.be.rejectedWith(Errors.ProjectNotFoundError)
  88. })
  89. it("should return a not found error if the user can't be found", async function () {
  90. this.UserGetter.promises.getUser
  91. .withArgs(this.collaborator._id)
  92. .resolves(null)
  93. await expect(
  94. this.handler.promises.transferOwnership(
  95. this.project._id,
  96. this.collaborator._id
  97. )
  98. ).to.be.rejectedWith(Errors.UserNotFoundError)
  99. })
  100. it('should return an error if user cannot be removed as collaborator ', async function () {
  101. this.CollaboratorsHandler.promises.removeUserFromProject.rejects(
  102. new Error('user-cannot-be-removed')
  103. )
  104. await expect(
  105. this.handler.promises.transferOwnership(
  106. this.project._id,
  107. this.collaborator._id
  108. )
  109. ).to.be.rejected
  110. })
  111. it('should transfer ownership of the project', async function () {
  112. await this.handler.promises.transferOwnership(
  113. this.project._id,
  114. this.collaborator._id
  115. )
  116. expect(this.ProjectModel.updateOne).to.have.been.calledWith(
  117. { _id: this.project._id },
  118. sinon.match({ $set: { owner_ref: this.collaborator._id } })
  119. )
  120. })
  121. it('should do nothing if transferring back to the owner', async function () {
  122. await this.handler.promises.transferOwnership(
  123. this.project._id,
  124. this.user._id
  125. )
  126. expect(this.ProjectModel.updateOne).not.to.have.been.called
  127. })
  128. it("should remove the user from the project's collaborators", async function () {
  129. await this.handler.promises.transferOwnership(
  130. this.project._id,
  131. this.collaborator._id
  132. )
  133. expect(
  134. this.CollaboratorsHandler.promises.removeUserFromProject
  135. ).to.have.been.calledWith(this.project._id, this.collaborator._id)
  136. })
  137. it('should add the former project owner as a read/write collaborator', async function () {
  138. await this.handler.promises.transferOwnership(
  139. this.project._id,
  140. this.collaborator._id
  141. )
  142. expect(
  143. this.CollaboratorsHandler.promises.addUserIdToProject
  144. ).to.have.been.calledWith(
  145. this.project._id,
  146. this.collaborator._id,
  147. this.user._id,
  148. PrivilegeLevels.READ_AND_WRITE
  149. )
  150. })
  151. it('should flush the project to tpds', async function () {
  152. await this.handler.promises.transferOwnership(
  153. this.project._id,
  154. this.collaborator._id
  155. )
  156. expect(
  157. this.TpdsProjectFlusher.promises.flushProjectToTpds
  158. ).to.have.been.calledWith(this.project._id)
  159. })
  160. it('should send an email notification', async function () {
  161. await this.handler.promises.transferOwnership(
  162. this.project._id,
  163. this.collaborator._id
  164. )
  165. expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
  166. 'ownershipTransferConfirmationPreviousOwner',
  167. {
  168. to: this.user.email,
  169. project: this.project,
  170. newOwner: this.collaborator,
  171. }
  172. )
  173. expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
  174. 'ownershipTransferConfirmationNewOwner',
  175. {
  176. to: this.collaborator.email,
  177. project: this.project,
  178. previousOwner: this.user,
  179. }
  180. )
  181. })
  182. it('should write an entry in the audit log', async function () {
  183. const sessionUserId = ObjectId()
  184. await this.handler.promises.transferOwnership(
  185. this.project._id,
  186. this.collaborator._id,
  187. { sessionUserId }
  188. )
  189. expect(
  190. this.ProjectAuditLogHandler.promises.addEntry
  191. ).to.have.been.calledWith(
  192. this.project._id,
  193. 'transfer-ownership',
  194. sessionUserId,
  195. {
  196. previousOwnerId: this.user._id,
  197. newOwnerId: this.collaborator._id,
  198. }
  199. )
  200. })
  201. it('should decline to transfer ownership to a non-collaborator', async function () {
  202. this.project.collaberator_refs = []
  203. await expect(
  204. this.handler.promises.transferOwnership(
  205. this.project._id,
  206. this.collaborator._id
  207. )
  208. ).to.be.rejectedWith(Errors.UserNotCollaboratorError)
  209. })
  210. })
  211. })