OwnershipTransferHandlerTests.js 7.1 KB

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