OwnershipTransferHandlerTests.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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. '../Analytics/AnalyticsManager': {
  72. recordEventForUser: (this.recordEventForUser = sinon.stub()),
  73. },
  74. },
  75. })
  76. })
  77. describe('transferOwnership', function () {
  78. beforeEach(function () {
  79. this.UserGetter.promises.getUser
  80. .withArgs(this.user._id)
  81. .resolves(this.user)
  82. this.UserGetter.promises.getUser
  83. .withArgs(this.collaborator._id)
  84. .resolves(this.collaborator)
  85. })
  86. it("should return a not found error if the project can't be found", async function () {
  87. this.ProjectGetter.promises.getProject.resolves(null)
  88. await expect(
  89. this.handler.promises.transferOwnership('abc', this.collaborator._id)
  90. ).to.be.rejectedWith(Errors.ProjectNotFoundError)
  91. })
  92. it("should return a not found error if the user can't be found", async function () {
  93. this.UserGetter.promises.getUser
  94. .withArgs(this.collaborator._id)
  95. .resolves(null)
  96. await expect(
  97. this.handler.promises.transferOwnership(
  98. this.project._id,
  99. this.collaborator._id
  100. )
  101. ).to.be.rejectedWith(Errors.UserNotFoundError)
  102. })
  103. it('should return an error if user cannot be removed as collaborator ', async function () {
  104. this.CollaboratorsHandler.promises.removeUserFromProject.rejects(
  105. new Error('user-cannot-be-removed')
  106. )
  107. await expect(
  108. this.handler.promises.transferOwnership(
  109. this.project._id,
  110. this.collaborator._id
  111. )
  112. ).to.be.rejected
  113. })
  114. it('should transfer ownership of the project', async function () {
  115. await this.handler.promises.transferOwnership(
  116. this.project._id,
  117. this.collaborator._id
  118. )
  119. expect(this.ProjectModel.updateOne).to.have.been.calledWith(
  120. { _id: this.project._id },
  121. sinon.match({ $set: { owner_ref: this.collaborator._id } })
  122. )
  123. })
  124. it('should do nothing if transferring back to the owner', async function () {
  125. await this.handler.promises.transferOwnership(
  126. this.project._id,
  127. this.user._id
  128. )
  129. expect(this.ProjectModel.updateOne).not.to.have.been.called
  130. })
  131. it("should remove the user from the project's collaborators", async function () {
  132. await this.handler.promises.transferOwnership(
  133. this.project._id,
  134. this.collaborator._id
  135. )
  136. expect(
  137. this.CollaboratorsHandler.promises.removeUserFromProject
  138. ).to.have.been.calledWith(this.project._id, this.collaborator._id)
  139. })
  140. it('should add the former project owner as a read/write collaborator', async function () {
  141. await this.handler.promises.transferOwnership(
  142. this.project._id,
  143. this.collaborator._id
  144. )
  145. expect(
  146. this.CollaboratorsHandler.promises.addUserIdToProject
  147. ).to.have.been.calledWith(
  148. this.project._id,
  149. this.collaborator._id,
  150. this.user._id,
  151. PrivilegeLevels.READ_AND_WRITE
  152. )
  153. })
  154. it('should flush the project to tpds', async function () {
  155. await this.handler.promises.transferOwnership(
  156. this.project._id,
  157. this.collaborator._id
  158. )
  159. expect(
  160. this.TpdsProjectFlusher.promises.flushProjectToTpds
  161. ).to.have.been.calledWith(this.project._id)
  162. })
  163. it('should send an email notification', async function () {
  164. await this.handler.promises.transferOwnership(
  165. this.project._id,
  166. this.collaborator._id
  167. )
  168. expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
  169. 'ownershipTransferConfirmationPreviousOwner',
  170. {
  171. to: this.user.email,
  172. project: this.project,
  173. newOwner: this.collaborator,
  174. }
  175. )
  176. expect(this.EmailHandler.promises.sendEmail).to.have.been.calledWith(
  177. 'ownershipTransferConfirmationNewOwner',
  178. {
  179. to: this.collaborator.email,
  180. project: this.project,
  181. previousOwner: this.user,
  182. }
  183. )
  184. })
  185. it('should track the change in BigQuery', async function () {
  186. const sessionUserId = ObjectId()
  187. await this.handler.promises.transferOwnership(
  188. this.project._id,
  189. this.collaborator._id,
  190. { sessionUserId }
  191. )
  192. expect(this.recordEventForUser).to.have.been.calledWith(
  193. this.user._id,
  194. 'project-ownership-transfer',
  195. {
  196. projectId: this.project._id,
  197. newOwnerId: this.collaborator._id,
  198. }
  199. )
  200. })
  201. it('should write an entry in the audit log', async function () {
  202. const sessionUserId = ObjectId()
  203. await this.handler.promises.transferOwnership(
  204. this.project._id,
  205. this.collaborator._id,
  206. { sessionUserId }
  207. )
  208. expect(
  209. this.ProjectAuditLogHandler.promises.addEntry
  210. ).to.have.been.calledWith(
  211. this.project._id,
  212. 'transfer-ownership',
  213. sessionUserId,
  214. {
  215. previousOwnerId: this.user._id,
  216. newOwnerId: this.collaborator._id,
  217. }
  218. )
  219. })
  220. it('should decline to transfer ownership to a non-collaborator', async function () {
  221. this.project.collaberator_refs = []
  222. await expect(
  223. this.handler.promises.transferOwnership(
  224. this.project._id,
  225. this.collaborator._id
  226. )
  227. ).to.be.rejectedWith(Errors.UserNotCollaboratorError)
  228. })
  229. })
  230. })