OwnershipTransferHandlerTests.js 7.9 KB

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