OwnershipTransferHandler.test.mjs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. import { vi, expect } from 'vitest'
  2. import sinon from 'sinon'
  3. import PrivilegeLevels from '../../../../app/src/Features/Authorization/PrivilegeLevels.mjs'
  4. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  5. import mongodb from 'mongodb-legacy'
  6. const { ObjectId } = mongodb
  7. const MODULE_PATH =
  8. '../../../../app/src/Features/Collaborators/OwnershipTransferHandler.mjs'
  9. describe('OwnershipTransferHandler', function () {
  10. beforeEach(async function (ctx) {
  11. ctx.user = { _id: new ObjectId(), email: 'owner@example.com' }
  12. ctx.collaborator = {
  13. _id: new ObjectId(),
  14. email: 'collaborator@example.com',
  15. }
  16. ctx.readOnlyCollaborator = {
  17. _id: new ObjectId(),
  18. email: 'readonly@example.com',
  19. }
  20. ctx.reviewer = {
  21. _id: new ObjectId(),
  22. email: 'reviewer@example.com',
  23. }
  24. ctx.project = {
  25. _id: new ObjectId(),
  26. name: 'project',
  27. owner_ref: ctx.user._id,
  28. collaberator_refs: [ctx.collaborator._id],
  29. readOnly_refs: [ctx.readOnlyCollaborator._id],
  30. reviewer_refs: [ctx.reviewer._id],
  31. }
  32. ctx.ProjectGetter = {
  33. promises: {
  34. getProject: sinon.stub().resolves(ctx.project),
  35. },
  36. }
  37. ctx.ProjectModel = {
  38. find: sinon.stub().resolves([]),
  39. updateOne: sinon.stub().returns({
  40. exec: sinon.stub().resolves(),
  41. }),
  42. }
  43. ctx.UserGetter = {
  44. promises: {
  45. getUser: sinon.stub().resolves(),
  46. },
  47. }
  48. ctx.TpdsUpdateSender = {
  49. promises: {
  50. moveEntity: sinon.stub().resolves(),
  51. },
  52. }
  53. ctx.TpdsProjectFlusher = {
  54. promises: {
  55. flushProjectToTpds: sinon.stub().resolves(),
  56. },
  57. }
  58. ctx.CollaboratorsHandler = {
  59. promises: {
  60. removeUserFromProject: sinon.stub().resolves(),
  61. addUserIdToProject: sinon.stub().resolves(),
  62. },
  63. }
  64. ctx.EmailHandler = {
  65. promises: {
  66. sendEmail: sinon.stub().resolves(),
  67. },
  68. }
  69. ctx.ProjectAuditLogHandler = {
  70. promises: {
  71. addEntry: sinon.stub().resolves(),
  72. },
  73. }
  74. ctx.TagsHandler = {
  75. promises: {
  76. createTag: sinon.stub().resolves(),
  77. addProjectsToTag: sinon.stub().resolves(),
  78. },
  79. }
  80. ctx.LimitationsManager = {
  81. promises: {
  82. canAddXEditCollaborators: sinon.stub().resolves(true),
  83. },
  84. }
  85. ctx.Modules = {
  86. promises: {
  87. hooks: {
  88. fire: sinon.stub().resolves(),
  89. },
  90. },
  91. }
  92. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  93. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  94. )
  95. vi.doMock('../../../../app/src/Features/Project/ProjectGetter.mjs', () => ({
  96. default: ctx.ProjectGetter,
  97. }))
  98. vi.doMock('../../../../app/src/models/Project.mjs', () => ({
  99. Project: ctx.ProjectModel,
  100. }))
  101. vi.doMock('../../../../app/src/Features/Tags/TagsHandler.mjs', () => ({
  102. default: ctx.TagsHandler,
  103. }))
  104. vi.doMock('../../../../app/src/Features/User/UserGetter.mjs', () => ({
  105. default: ctx.UserGetter,
  106. }))
  107. vi.doMock(
  108. '../../../../app/src/Features/ThirdPartyDataStore/TpdsProjectFlusher.mjs',
  109. () => ({
  110. default: ctx.TpdsProjectFlusher,
  111. })
  112. )
  113. vi.doMock(
  114. '../../../../app/src/Features/Project/ProjectAuditLogHandler.mjs',
  115. () => ({
  116. default: ctx.ProjectAuditLogHandler,
  117. })
  118. )
  119. vi.doMock('../../../../app/src/Features/Email/EmailHandler.mjs', () => ({
  120. default: ctx.EmailHandler,
  121. }))
  122. vi.doMock(
  123. '../../../../app/src/Features/Collaborators/CollaboratorsHandler.mjs',
  124. () => ({
  125. default: ctx.CollaboratorsHandler,
  126. })
  127. )
  128. vi.doMock(
  129. '../../../../app/src/Features/Analytics/AnalyticsManager',
  130. () => ({
  131. default: {
  132. recordEventForUserInBackground: (ctx.recordEventForUserInBackground =
  133. sinon.stub()),
  134. },
  135. })
  136. )
  137. vi.doMock(
  138. '../../../../app/src/Features/Subscription/LimitationsManager.mjs',
  139. () => ({
  140. default: ctx.LimitationsManager,
  141. })
  142. )
  143. vi.doMock('../../../../app/src/infrastructure/Modules.mjs', () => ({
  144. default: ctx.Modules,
  145. }))
  146. ctx.handler = (await import(MODULE_PATH)).default
  147. })
  148. describe('transferOwnership', function () {
  149. beforeEach(function (ctx) {
  150. ctx.UserGetter.promises.getUser.withArgs(ctx.user._id).resolves(ctx.user)
  151. ctx.UserGetter.promises.getUser
  152. .withArgs(ctx.collaborator._id)
  153. .resolves(ctx.collaborator)
  154. ctx.UserGetter.promises.getUser
  155. .withArgs(ctx.readOnlyCollaborator._id)
  156. .resolves(ctx.readOnlyCollaborator)
  157. ctx.UserGetter.promises.getUser
  158. .withArgs(ctx.reviewer._id)
  159. .resolves(ctx.reviewer)
  160. })
  161. it("should return a not found error if the project can't be found", async function (ctx) {
  162. ctx.ProjectGetter.promises.getProject.resolves(null)
  163. await expect(
  164. ctx.handler.promises.transferOwnership('abc', ctx.collaborator._id)
  165. ).to.be.rejectedWith(Errors.ProjectNotFoundError)
  166. })
  167. it("should return a not found error if the user can't be found", async function (ctx) {
  168. ctx.UserGetter.promises.getUser
  169. .withArgs(ctx.collaborator._id)
  170. .resolves(null)
  171. await expect(
  172. ctx.handler.promises.transferOwnership(
  173. ctx.project._id,
  174. ctx.collaborator._id
  175. )
  176. ).to.be.rejectedWith(Errors.UserNotFoundError)
  177. })
  178. it('should return an error if user cannot be removed as collaborator ', async function (ctx) {
  179. ctx.CollaboratorsHandler.promises.removeUserFromProject.rejects(
  180. new Error('user-cannot-be-removed')
  181. )
  182. await expect(
  183. ctx.handler.promises.transferOwnership(
  184. ctx.project._id,
  185. ctx.collaborator._id
  186. )
  187. ).to.be.rejected
  188. })
  189. it('should transfer ownership of the project', async function (ctx) {
  190. await ctx.handler.promises.transferOwnership(
  191. ctx.project._id,
  192. ctx.collaborator._id
  193. )
  194. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  195. { _id: ctx.project._id },
  196. sinon.match({ $set: { owner_ref: ctx.collaborator._id } })
  197. )
  198. })
  199. it('should check collaborator limits after ownership transfer', async function (ctx) {
  200. await ctx.handler.promises.transferOwnership(
  201. ctx.project._id,
  202. ctx.collaborator._id
  203. )
  204. expect(ctx.ProjectModel.updateOne).to.have.been.calledBefore(
  205. ctx.LimitationsManager.promises.canAddXEditCollaborators
  206. )
  207. expect(
  208. ctx.LimitationsManager.promises.canAddXEditCollaborators
  209. ).to.have.been.calledBefore(
  210. ctx.CollaboratorsHandler.promises.addUserIdToProject
  211. )
  212. })
  213. describe('when there are edit collaborator slots available', function () {
  214. beforeEach(function (ctx) {
  215. ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(true)
  216. })
  217. it('should give old owner read/write permissions when transferring to a read-only collaborator', async function (ctx) {
  218. await ctx.handler.promises.transferOwnership(
  219. ctx.project._id,
  220. ctx.readOnlyCollaborator._id
  221. )
  222. expect(
  223. ctx.CollaboratorsHandler.promises.addUserIdToProject
  224. ).to.have.been.calledWith(
  225. ctx.project._id,
  226. ctx.readOnlyCollaborator._id,
  227. ctx.user._id,
  228. PrivilegeLevels.READ_AND_WRITE,
  229. undefined
  230. )
  231. expect(
  232. ctx.LimitationsManager.promises.canAddXEditCollaborators
  233. ).to.have.been.calledWith(ctx.project._id, 1)
  234. })
  235. it('should give old owner read/write permissions when transferring to an editor', async function (ctx) {
  236. await ctx.handler.promises.transferOwnership(
  237. ctx.project._id,
  238. ctx.collaborator._id
  239. )
  240. expect(
  241. ctx.CollaboratorsHandler.promises.addUserIdToProject
  242. ).to.have.been.calledWith(
  243. ctx.project._id,
  244. ctx.collaborator._id,
  245. ctx.user._id,
  246. PrivilegeLevels.READ_AND_WRITE,
  247. undefined
  248. )
  249. expect(
  250. ctx.LimitationsManager.promises.canAddXEditCollaborators
  251. ).to.have.been.calledWith(ctx.project._id, 1)
  252. })
  253. it('should give old owner read/write permissions when transferring to a reviewer', async function (ctx) {
  254. await ctx.handler.promises.transferOwnership(
  255. ctx.project._id,
  256. ctx.reviewer._id
  257. )
  258. expect(
  259. ctx.CollaboratorsHandler.promises.addUserIdToProject
  260. ).to.have.been.calledWith(
  261. ctx.project._id,
  262. ctx.reviewer._id,
  263. ctx.user._id,
  264. PrivilegeLevels.READ_AND_WRITE,
  265. undefined
  266. )
  267. expect(
  268. ctx.LimitationsManager.promises.canAddXEditCollaborators
  269. ).to.have.been.calledWith(ctx.project._id, 1)
  270. })
  271. it('should give old owner read/write permissions when transferring to a non-collaborator', async function (ctx) {
  272. ctx.project.collaberator_refs = []
  273. ctx.project.readOnly_refs = []
  274. ctx.project.reviewer_refs = []
  275. const newOwner = {
  276. _id: new ObjectId(),
  277. email: 'admin@example.com',
  278. }
  279. ctx.UserGetter.promises.getUser
  280. .withArgs(newOwner._id)
  281. .resolves(newOwner)
  282. await ctx.handler.promises.transferOwnership(
  283. ctx.project._id,
  284. newOwner._id,
  285. { allowTransferToNonCollaborators: true }
  286. )
  287. expect(
  288. ctx.CollaboratorsHandler.promises.addUserIdToProject
  289. ).to.have.been.calledWith(
  290. ctx.project._id,
  291. newOwner._id,
  292. ctx.user._id,
  293. PrivilegeLevels.READ_AND_WRITE,
  294. undefined
  295. )
  296. expect(
  297. ctx.LimitationsManager.promises.canAddXEditCollaborators
  298. ).to.have.been.calledWith(ctx.project._id, 1)
  299. })
  300. })
  301. describe('when there are no edit collaborator slots available', function () {
  302. beforeEach(function (ctx) {
  303. ctx.LimitationsManager.promises.canAddXEditCollaborators.resolves(false)
  304. })
  305. it('should give old owner read-only with pending editor flag when transferring to an editor', async function (ctx) {
  306. await ctx.handler.promises.transferOwnership(
  307. ctx.project._id,
  308. ctx.collaborator._id
  309. )
  310. expect(
  311. ctx.CollaboratorsHandler.promises.addUserIdToProject
  312. ).to.have.been.calledWith(
  313. ctx.project._id,
  314. ctx.collaborator._id,
  315. ctx.user._id,
  316. PrivilegeLevels.READ_ONLY,
  317. { pendingEditor: true }
  318. )
  319. })
  320. it('should give old owner read-only with pending editor flag when transferring to a reviewer', async function (ctx) {
  321. await ctx.handler.promises.transferOwnership(
  322. ctx.project._id,
  323. ctx.reviewer._id
  324. )
  325. expect(
  326. ctx.CollaboratorsHandler.promises.addUserIdToProject
  327. ).to.have.been.calledWith(
  328. ctx.project._id,
  329. ctx.reviewer._id,
  330. ctx.user._id,
  331. PrivilegeLevels.READ_ONLY,
  332. { pendingEditor: true }
  333. )
  334. })
  335. it('should give old owner read-only with pending editor flag when transferring to a read-only collaborator', async function (ctx) {
  336. await ctx.handler.promises.transferOwnership(
  337. ctx.project._id,
  338. ctx.readOnlyCollaborator._id
  339. )
  340. expect(
  341. ctx.CollaboratorsHandler.promises.addUserIdToProject
  342. ).to.have.been.calledWith(
  343. ctx.project._id,
  344. ctx.readOnlyCollaborator._id,
  345. ctx.user._id,
  346. PrivilegeLevels.READ_ONLY,
  347. { pendingEditor: true }
  348. )
  349. })
  350. it('should give old owner read-only with pending editor flag when transferring to a non-collaborator', async function (ctx) {
  351. ctx.project.collaberator_refs = []
  352. ctx.project.readOnly_refs = []
  353. ctx.project.reviewer_refs = []
  354. const newOwner = {
  355. _id: new ObjectId(),
  356. email: 'newowner@example.com',
  357. }
  358. ctx.UserGetter.promises.getUser
  359. .withArgs(newOwner._id)
  360. .resolves(newOwner)
  361. await ctx.handler.promises.transferOwnership(
  362. ctx.project._id,
  363. newOwner._id,
  364. { allowTransferToNonCollaborators: true }
  365. )
  366. expect(
  367. ctx.CollaboratorsHandler.promises.addUserIdToProject
  368. ).to.have.been.calledWith(
  369. ctx.project._id,
  370. newOwner._id,
  371. ctx.user._id,
  372. PrivilegeLevels.READ_ONLY,
  373. { pendingEditor: true }
  374. )
  375. })
  376. })
  377. it('should do nothing if transferring back to the owner', async function (ctx) {
  378. await ctx.handler.promises.transferOwnership(
  379. ctx.project._id,
  380. ctx.user._id
  381. )
  382. expect(ctx.ProjectModel.updateOne).not.to.have.been.called
  383. })
  384. it("should remove the user from the project's collaborators", async function (ctx) {
  385. await ctx.handler.promises.transferOwnership(
  386. ctx.project._id,
  387. ctx.collaborator._id
  388. )
  389. expect(
  390. ctx.CollaboratorsHandler.promises.removeUserFromProject
  391. ).to.have.been.calledWith(ctx.project._id, ctx.collaborator._id)
  392. })
  393. it('should transfer ownership of the project to a reviewer', async function (ctx) {
  394. await ctx.handler.promises.transferOwnership(
  395. ctx.project._id,
  396. ctx.reviewer._id
  397. )
  398. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  399. { _id: ctx.project._id },
  400. sinon.match({ $set: { owner_ref: ctx.reviewer._id } })
  401. )
  402. })
  403. it('should flush the project to tpds', async function (ctx) {
  404. await ctx.handler.promises.transferOwnership(
  405. ctx.project._id,
  406. ctx.collaborator._id
  407. )
  408. expect(
  409. ctx.TpdsProjectFlusher.promises.flushProjectToTpds
  410. ).to.have.been.calledWith(ctx.project._id)
  411. })
  412. it('should send an email notification', async function (ctx) {
  413. await ctx.handler.promises.transferOwnership(
  414. ctx.project._id,
  415. ctx.collaborator._id
  416. )
  417. expect(ctx.EmailHandler.promises.sendEmail).to.have.been.calledWith(
  418. 'ownershipTransferConfirmationPreviousOwner',
  419. {
  420. to: ctx.user.email,
  421. project: ctx.project,
  422. newOwner: ctx.collaborator,
  423. }
  424. )
  425. expect(ctx.EmailHandler.promises.sendEmail).to.have.been.calledWith(
  426. 'ownershipTransferConfirmationNewOwner',
  427. {
  428. to: ctx.collaborator.email,
  429. project: ctx.project,
  430. previousOwner: ctx.user,
  431. }
  432. )
  433. })
  434. it('should not send an email notification with the skipEmails option', async function (ctx) {
  435. await ctx.handler.promises.transferOwnership(
  436. ctx.project._id,
  437. ctx.collaborator._id,
  438. { skipEmails: true }
  439. )
  440. expect(ctx.EmailHandler.promises.sendEmail).not.to.have.been.called
  441. })
  442. it('should track the change in BigQuery', async function (ctx) {
  443. const sessionUserId = new ObjectId()
  444. await ctx.handler.promises.transferOwnership(
  445. ctx.project._id,
  446. ctx.collaborator._id,
  447. { sessionUserId }
  448. )
  449. expect(ctx.recordEventForUserInBackground).to.have.been.calledWith(
  450. ctx.user._id,
  451. 'project-ownership-transfer',
  452. {
  453. projectId: ctx.project._id,
  454. newOwnerId: ctx.collaborator._id,
  455. }
  456. )
  457. })
  458. it('should write an entry in the audit log', async function (ctx) {
  459. const sessionUserId = new ObjectId()
  460. const ipAddress = '1.2.3.4'
  461. await ctx.handler.promises.transferOwnership(
  462. ctx.project._id,
  463. ctx.collaborator._id,
  464. { sessionUserId, ipAddress }
  465. )
  466. expect(
  467. ctx.ProjectAuditLogHandler.promises.addEntry
  468. ).to.have.been.calledWith(
  469. ctx.project._id,
  470. 'transfer-ownership',
  471. sessionUserId,
  472. ipAddress,
  473. {
  474. previousOwnerId: ctx.user._id,
  475. newOwnerId: ctx.collaborator._id,
  476. }
  477. )
  478. })
  479. it('should fire the ownershipTransferred hook', async function (ctx) {
  480. await ctx.handler.promises.transferOwnership(
  481. ctx.project._id,
  482. ctx.collaborator._id
  483. )
  484. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  485. 'projectOwnershipTransferred',
  486. ctx.project._id,
  487. ctx.user._id,
  488. ctx.collaborator._id
  489. )
  490. })
  491. it('should decline to transfer ownership to a non-collaborator', async function (ctx) {
  492. ctx.project.collaberator_refs = []
  493. ctx.project.readOnly_refs = []
  494. await expect(
  495. ctx.handler.promises.transferOwnership(
  496. ctx.project._id,
  497. ctx.collaborator._id
  498. )
  499. ).to.be.rejectedWith(Errors.UserNotCollaboratorError)
  500. })
  501. })
  502. describe('transferAllProjectsToUser', function () {
  503. const fromUserEmail = 'user.one@example.com'
  504. const ipAddress = '1.2.3.4'
  505. let fromUserId, toUserId
  506. beforeEach(function () {
  507. fromUserId = new ObjectId().toString()
  508. toUserId = new ObjectId().toString()
  509. })
  510. describe('with missing user', function () {
  511. it('should throw an error', async function (ctx) {
  512. ctx.UserGetter.promises.getUser.withArgs(fromUserId).resolves(null)
  513. ctx.UserGetter.promises.getUser
  514. .withArgs(toUserId)
  515. .resolves({ _id: new ObjectId(toUserId) })
  516. await expect(
  517. ctx.handler.promises.transferAllProjectsToUser({
  518. toUserId,
  519. fromUserId,
  520. ipAddress,
  521. })
  522. ).to.be.rejectedWith(/missing source user/)
  523. ctx.UserGetter.promises.getUser
  524. .withArgs(fromUserId)
  525. .resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
  526. ctx.UserGetter.promises.getUser.withArgs(toUserId).resolves(null)
  527. await expect(
  528. ctx.handler.promises.transferAllProjectsToUser({
  529. fromUserId,
  530. toUserId,
  531. ipAddress,
  532. })
  533. ).to.be.rejectedWith(/missing destination user/)
  534. })
  535. })
  536. describe('with the same id', function () {
  537. it('should throw an error', async function (ctx) {
  538. ctx.UserGetter.promises.getUser
  539. .withArgs(fromUserId)
  540. .resolves({ _id: new ObjectId(fromUserId), email: fromUserEmail })
  541. await expect(
  542. ctx.handler.promises.transferAllProjectsToUser({
  543. fromUserId,
  544. toUserId: fromUserId,
  545. ipAddress,
  546. })
  547. ).to.be.rejectedWith(/rejecting transfer between identical users/)
  548. })
  549. })
  550. describe('happy path', function () {
  551. let tag, fromUserEmail, projects
  552. beforeEach(function (ctx) {
  553. tag = {
  554. _id: new ObjectId(),
  555. name: 'some-tag-name',
  556. }
  557. projects = [
  558. { _id: 'project-1' },
  559. { _id: 'project-2' },
  560. { _id: 'project-3' },
  561. ]
  562. ctx.UserGetter.promises.getUser.withArgs(fromUserId).resolves({
  563. _id: new ObjectId(fromUserId),
  564. email: fromUserEmail,
  565. })
  566. ctx.UserGetter.promises.getUser.withArgs(toUserId).resolves({
  567. _id: new ObjectId(toUserId),
  568. })
  569. ctx.ProjectModel.find.resolves(projects)
  570. ctx.TagsHandler.promises.createTag.resolves({
  571. _id: tag._id,
  572. name: 'some-tag-name',
  573. })
  574. ctx.TagsHandler.promises.addProjectsToTag.resolves()
  575. })
  576. it('creates a tag', async function (ctx) {
  577. await ctx.handler.promises.transferAllProjectsToUser({
  578. fromUserId,
  579. toUserId,
  580. ipAddress,
  581. })
  582. expect(ctx.TagsHandler.promises.createTag).to.have.been.calledWith(
  583. toUserId,
  584. `transferred-from-${fromUserEmail}`,
  585. '#434AF0',
  586. { truncate: true }
  587. )
  588. })
  589. it('returns a projectCount, and tag name', async function (ctx) {
  590. const result = await ctx.handler.promises.transferAllProjectsToUser({
  591. fromUserId,
  592. toUserId,
  593. ipAddress,
  594. })
  595. expect(result.projectCount).to.equal(projects.length)
  596. expect(result.newTagName).to.equal('some-tag-name')
  597. })
  598. it('gets the user records', async function (ctx) {
  599. await ctx.handler.promises.transferAllProjectsToUser({
  600. fromUserId,
  601. toUserId,
  602. ipAddress,
  603. })
  604. expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith(
  605. fromUserId
  606. )
  607. expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith(
  608. toUserId
  609. )
  610. })
  611. it('gets the list of affected projects', async function (ctx) {
  612. await ctx.handler.promises.transferAllProjectsToUser({
  613. fromUserId,
  614. toUserId,
  615. ipAddress,
  616. })
  617. expect(ctx.ProjectModel.find).to.have.been.calledWith({
  618. owner_ref: fromUserId,
  619. })
  620. })
  621. it('transfers all of the projects', async function (ctx) {
  622. await ctx.handler.promises.transferAllProjectsToUser({
  623. fromUserId,
  624. toUserId,
  625. ipAddress,
  626. })
  627. expect(ctx.ProjectModel.updateOne.callCount).to.equal(3)
  628. expect(ctx.TagsHandler.promises.addProjectsToTag.callCount).to.equal(1)
  629. for (const project of projects) {
  630. expect(ctx.ProjectModel.updateOne).to.have.been.calledWith(
  631. { _id: project._id },
  632. sinon.match({ $set: { owner_ref: toUserId } })
  633. )
  634. }
  635. expect(
  636. ctx.TagsHandler.promises.addProjectsToTag
  637. ).to.have.been.calledWith(
  638. toUserId,
  639. tag._id,
  640. projects.map(p => p._id)
  641. )
  642. })
  643. })
  644. })
  645. })