OwnershipTransferHandlerTests.js 8.0 KB

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