OwnershipTransferHandlerTests.js 9.2 KB

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