TpdsUpdateHandler.test.mjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import { expect, vi } from 'vitest'
  2. import sinon from 'sinon'
  3. import mongodb from 'mongodb-legacy'
  4. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  5. const ObjectId = mongodb.ObjectId
  6. const MODULE_PATH =
  7. '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.mjs'
  8. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  9. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  10. )
  11. describe('TpdsUpdateHandler', function () {
  12. beforeEach(async function (ctx) {
  13. ctx.projectName = 'My recipes'
  14. ctx.userId = new ObjectId()
  15. ctx.projects = {
  16. active1: { _id: new ObjectId(), name: ctx.projectName },
  17. active2: { _id: new ObjectId(), name: ctx.projectName },
  18. archived1: {
  19. _id: new ObjectId(),
  20. name: ctx.projectName,
  21. archived: [ctx.userId],
  22. },
  23. archived2: {
  24. _id: new ObjectId(),
  25. name: ctx.projectName,
  26. archived: [ctx.userId],
  27. },
  28. trashed: {
  29. _id: new ObjectId(),
  30. name: ctx.projectName,
  31. trashed: [ctx.userId],
  32. },
  33. }
  34. ctx.source = 'dropbox'
  35. ctx.path = `/some/file`
  36. ctx.update = {}
  37. ctx.folderPath = '/some/folder'
  38. ctx.folder = {
  39. _id: new ObjectId(),
  40. parentFolder_id: new ObjectId(),
  41. }
  42. ctx.CooldownManager = {
  43. isProjectOnCooldown: sinon.stub().resolves(false),
  44. }
  45. ctx.FileTypeManager = {
  46. shouldIgnore: sinon.stub().returns(false),
  47. }
  48. ctx.Modules = {
  49. promises: {
  50. hooks: { fire: sinon.stub().resolves() },
  51. },
  52. }
  53. ctx.notification = {
  54. create: sinon.stub().resolves(),
  55. }
  56. ctx.NotificationsBuilder = {
  57. promises: {
  58. dropboxDuplicateProjectNames: sinon.stub().returns(ctx.notification),
  59. },
  60. }
  61. ctx.ProjectCreationHandler = {
  62. promises: {
  63. createBlankProject: sinon.stub().resolves(ctx.projects.active1),
  64. },
  65. }
  66. ctx.ProjectDeleter = {
  67. promises: {
  68. markAsDeletedByExternalSource: sinon.stub().resolves(),
  69. },
  70. }
  71. ctx.ProjectGetter = {
  72. promises: {
  73. findUsersProjectsByName: sinon.stub(),
  74. findAllUsersProjects: sinon.stub().resolves({
  75. owned: Object.values(ctx.projects),
  76. readAndWrite: [],
  77. }),
  78. },
  79. }
  80. ctx.ProjectHelper = {
  81. isArchivedOrTrashed: sinon.stub().returns(false),
  82. }
  83. ctx.ProjectHelper.isArchivedOrTrashed
  84. .withArgs(ctx.projects.archived1, ctx.userId)
  85. .returns(true)
  86. ctx.ProjectHelper.isArchivedOrTrashed
  87. .withArgs(ctx.projects.archived2, ctx.userId)
  88. .returns(true)
  89. ctx.ProjectHelper.isArchivedOrTrashed
  90. .withArgs(ctx.projects.trashed, ctx.userId)
  91. .returns(true)
  92. ctx.RootDocManager = {
  93. setRootDocAutomaticallyInBackground: sinon.stub(),
  94. }
  95. ctx.UpdateMerger = {
  96. promises: {
  97. deleteUpdate: sinon.stub().resolves(),
  98. mergeUpdate: sinon.stub().resolves(),
  99. createFolder: sinon.stub().resolves(ctx.folder),
  100. },
  101. }
  102. vi.doMock('../../../../app/src/Features/Cooldown/CooldownManager', () => ({
  103. default: ctx.CooldownManager,
  104. }))
  105. vi.doMock('../../../../app/src/Features/Uploads/FileTypeManager', () => ({
  106. default: ctx.FileTypeManager,
  107. }))
  108. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  109. default: ctx.Modules,
  110. }))
  111. vi.doMock(
  112. '../../../../app/src/Features/Notifications/NotificationsBuilder',
  113. () => ({
  114. default: ctx.NotificationsBuilder,
  115. })
  116. )
  117. vi.doMock(
  118. '../../../../app/src/Features/Project/ProjectCreationHandler',
  119. () => ({
  120. default: ctx.ProjectCreationHandler,
  121. })
  122. )
  123. vi.doMock('../../../../app/src/Features/Project/ProjectDeleter', () => ({
  124. default: ctx.ProjectDeleter,
  125. }))
  126. vi.doMock('../../../../app/src/Features/Project/ProjectGetter', () => ({
  127. default: ctx.ProjectGetter,
  128. }))
  129. vi.doMock('../../../../app/src/Features/Project/ProjectHelper', () => ({
  130. default: ctx.ProjectHelper,
  131. }))
  132. vi.doMock(
  133. '../../../../app/src/Features/Project/ProjectRootDocManager',
  134. () => ({
  135. default: ctx.RootDocManager,
  136. })
  137. )
  138. vi.doMock(
  139. '../../../../app/src/Features/ThirdPartyDataStore/UpdateMerger',
  140. () => ({
  141. default: ctx.UpdateMerger,
  142. })
  143. )
  144. ctx.TpdsUpdateHandler = (await import(MODULE_PATH)).default
  145. })
  146. describe('getting an update', function () {
  147. describe('byId', function () {
  148. describe('with no matching project', function () {
  149. beforeEach(function (ctx) {
  150. ctx.projectId = new ObjectId().toString()
  151. })
  152. receiveUpdateById()
  153. expectProjectNotCreated()
  154. expectUpdateNotProcessed()
  155. })
  156. describe('with one matching active project', function () {
  157. beforeEach(function (ctx) {
  158. ctx.projectId = ctx.projects.active1._id.toString()
  159. })
  160. receiveUpdateById()
  161. expectProjectNotCreated()
  162. expectUpdateProcessed()
  163. })
  164. describe('with one matching archived project', function () {
  165. beforeEach(function (ctx) {
  166. ctx.projectId = ctx.projects.archived1._id.toString()
  167. })
  168. receiveUpdateById()
  169. expectProjectNotCreated()
  170. expectUpdateNotProcessed()
  171. })
  172. describe('with one matching trashed project', function () {
  173. beforeEach(function (ctx) {
  174. ctx.projectId = ctx.projects.trashed._id.toString()
  175. })
  176. receiveUpdateById()
  177. expectProjectNotCreated()
  178. expectUpdateNotProcessed()
  179. })
  180. })
  181. describe('with no matching project', function () {
  182. setupMatchingProjects([])
  183. receiveUpdate()
  184. expectProjectCreated()
  185. expectUpdateProcessed()
  186. })
  187. describe('with one matching active project', function () {
  188. setupMatchingProjects(['active1'])
  189. receiveUpdate()
  190. expectProjectNotCreated()
  191. expectUpdateProcessed()
  192. })
  193. describe('with one matching archived project', function () {
  194. setupMatchingProjects(['archived1'])
  195. receiveUpdate()
  196. expectProjectNotCreated()
  197. expectUpdateNotProcessed()
  198. expectDropboxNotUnlinked()
  199. })
  200. describe('with two matching active projects', function () {
  201. setupMatchingProjects(['active1', 'active2'])
  202. receiveUpdate()
  203. expectProjectNotCreated()
  204. expectUpdateNotProcessed()
  205. expectDropboxUnlinked()
  206. })
  207. describe('with two matching archived projects', function () {
  208. setupMatchingProjects(['archived1', 'archived2'])
  209. receiveUpdate()
  210. expectProjectNotCreated()
  211. expectUpdateNotProcessed()
  212. expectDropboxNotUnlinked()
  213. })
  214. describe('with one matching active and one matching archived project', function () {
  215. setupMatchingProjects(['active1', 'archived1'])
  216. receiveUpdate()
  217. expectProjectNotCreated()
  218. expectUpdateNotProcessed()
  219. expectDropboxUnlinked()
  220. })
  221. describe('update to a file that should be ignored', async function () {
  222. setupMatchingProjects(['active1'])
  223. beforeEach(function (ctx) {
  224. ctx.FileTypeManager.shouldIgnore.returns(true)
  225. })
  226. receiveUpdate()
  227. expectProjectNotCreated()
  228. expectUpdateNotProcessed()
  229. expectDropboxNotUnlinked()
  230. })
  231. describe('update to a project on cooldown', async function () {
  232. setupMatchingProjects(['active1'])
  233. setupProjectOnCooldown()
  234. beforeEach(async function (ctx) {
  235. await expect(
  236. ctx.TpdsUpdateHandler.promises.newUpdate(
  237. ctx.userId,
  238. '', // projectId
  239. ctx.projectName,
  240. ctx.path,
  241. ctx.update,
  242. ctx.source
  243. )
  244. ).to.be.rejectedWith(Errors.TooManyRequestsError)
  245. })
  246. expectUpdateNotProcessed()
  247. })
  248. })
  249. describe('getting a file delete', function () {
  250. describe('byId', function () {
  251. describe('with no matching project', function () {
  252. beforeEach(function (ctx) {
  253. ctx.projectId = new ObjectId().toString()
  254. })
  255. receiveFileDeleteById()
  256. expectDeleteNotProcessed()
  257. expectProjectNotDeleted()
  258. })
  259. describe('with one matching active project', function () {
  260. beforeEach(function (ctx) {
  261. ctx.projectId = ctx.projects.active1._id.toString()
  262. })
  263. receiveFileDeleteById()
  264. expectDeleteProcessed()
  265. expectProjectNotDeleted()
  266. })
  267. describe('with one matching archived project', function () {
  268. beforeEach(function (ctx) {
  269. ctx.projectId = ctx.projects.archived1._id.toString()
  270. })
  271. receiveFileDeleteById()
  272. expectDeleteNotProcessed()
  273. expectProjectNotDeleted()
  274. })
  275. describe('with one matching trashed project', function () {
  276. beforeEach(function (ctx) {
  277. ctx.projectId = ctx.projects.trashed._id.toString()
  278. })
  279. receiveFileDeleteById()
  280. expectDeleteNotProcessed()
  281. expectProjectNotDeleted()
  282. })
  283. })
  284. describe('with no matching project', function () {
  285. setupMatchingProjects([])
  286. receiveFileDelete()
  287. expectDeleteNotProcessed()
  288. expectProjectNotDeleted()
  289. })
  290. describe('with one matching active project', function () {
  291. setupMatchingProjects(['active1'])
  292. receiveFileDelete()
  293. expectDeleteProcessed()
  294. expectProjectNotDeleted()
  295. })
  296. describe('with one matching archived project', function () {
  297. setupMatchingProjects(['archived1'])
  298. receiveFileDelete()
  299. expectDeleteNotProcessed()
  300. expectProjectNotDeleted()
  301. })
  302. describe('with two matching active projects', function () {
  303. setupMatchingProjects(['active1', 'active2'])
  304. receiveFileDelete()
  305. expectDeleteNotProcessed()
  306. expectProjectNotDeleted()
  307. expectDropboxUnlinked()
  308. })
  309. describe('with two matching archived projects', function () {
  310. setupMatchingProjects(['archived1', 'archived2'])
  311. receiveFileDelete()
  312. expectDeleteNotProcessed()
  313. expectProjectNotDeleted()
  314. expectDropboxNotUnlinked()
  315. })
  316. describe('with one matching active and one matching archived project', function () {
  317. setupMatchingProjects(['active1', 'archived1'])
  318. receiveFileDelete()
  319. expectDeleteNotProcessed()
  320. expectProjectNotDeleted()
  321. expectDropboxUnlinked()
  322. })
  323. })
  324. describe('getting a project delete', function () {
  325. describe('with no matching project', function () {
  326. setupMatchingProjects([])
  327. receiveProjectDelete()
  328. expectDeleteNotProcessed()
  329. expectProjectNotDeleted()
  330. })
  331. describe('with one matching active project', function () {
  332. setupMatchingProjects(['active1'])
  333. receiveProjectDelete()
  334. expectDeleteNotProcessed()
  335. expectProjectDeleted()
  336. })
  337. describe('with one matching archived project', function () {
  338. setupMatchingProjects(['archived1'])
  339. receiveProjectDelete()
  340. expectDeleteNotProcessed()
  341. expectProjectNotDeleted()
  342. })
  343. describe('with two matching active projects', function () {
  344. setupMatchingProjects(['active1', 'active2'])
  345. receiveProjectDelete()
  346. expectDeleteNotProcessed()
  347. expectProjectNotDeleted()
  348. expectDropboxUnlinked()
  349. })
  350. describe('with two matching archived projects', function () {
  351. setupMatchingProjects(['archived1', 'archived2'])
  352. receiveProjectDelete()
  353. expectDeleteNotProcessed()
  354. expectProjectNotDeleted()
  355. expectDropboxNotUnlinked()
  356. })
  357. describe('with one matching active and one matching archived project', function () {
  358. setupMatchingProjects(['active1', 'archived1'])
  359. receiveProjectDelete()
  360. expectDeleteNotProcessed()
  361. expectProjectNotDeleted()
  362. expectDropboxUnlinked()
  363. })
  364. })
  365. describe('getting a folder update', function () {
  366. describe('with no matching project', function () {
  367. setupMatchingProjects([])
  368. receiveFolderUpdate()
  369. expectProjectCreated()
  370. expectFolderUpdateProcessed()
  371. })
  372. describe('with one matching active project', function () {
  373. setupMatchingProjects(['active1'])
  374. receiveFolderUpdate()
  375. expectProjectNotCreated()
  376. expectFolderUpdateProcessed()
  377. })
  378. describe('with one matching archived project', function () {
  379. setupMatchingProjects(['archived1'])
  380. receiveFolderUpdate()
  381. expectProjectNotCreated()
  382. expectFolderUpdateNotProcessed()
  383. expectDropboxNotUnlinked()
  384. })
  385. describe('with two matching active projects', function () {
  386. setupMatchingProjects(['active1', 'active2'])
  387. receiveFolderUpdate()
  388. expectProjectNotCreated()
  389. expectFolderUpdateNotProcessed()
  390. expectDropboxUnlinked()
  391. })
  392. describe('with two matching archived projects', function () {
  393. setupMatchingProjects(['archived1', 'archived2'])
  394. receiveFolderUpdate()
  395. expectProjectNotCreated()
  396. expectFolderUpdateNotProcessed()
  397. expectDropboxNotUnlinked()
  398. })
  399. describe('with one matching active and one matching archived project', function () {
  400. setupMatchingProjects(['active1', 'archived1'])
  401. receiveFolderUpdate()
  402. expectProjectNotCreated()
  403. expectFolderUpdateNotProcessed()
  404. expectDropboxUnlinked()
  405. })
  406. describe('update to a project on cooldown', async function () {
  407. setupMatchingProjects(['active1'])
  408. setupProjectOnCooldown()
  409. beforeEach(async function (ctx) {
  410. await expect(
  411. ctx.TpdsUpdateHandler.promises.createFolder(
  412. ctx.userId,
  413. ctx.projectId,
  414. ctx.projectName,
  415. ctx.path
  416. )
  417. ).to.be.rejectedWith(Errors.TooManyRequestsError)
  418. })
  419. expectFolderUpdateNotProcessed()
  420. })
  421. })
  422. })
  423. /* Setup helpers */
  424. function setupMatchingProjects(projectKeys) {
  425. beforeEach(function (ctx) {
  426. const projects = projectKeys.map(key => ctx.projects[key])
  427. ctx.ProjectGetter.promises.findUsersProjectsByName
  428. .withArgs(ctx.userId, ctx.projectName)
  429. .resolves(projects)
  430. })
  431. }
  432. function setupProjectOnCooldown() {
  433. beforeEach(function (ctx) {
  434. ctx.CooldownManager.isProjectOnCooldown
  435. .withArgs(ctx.projects.active1._id)
  436. .resolves(true)
  437. })
  438. }
  439. /* Test helpers */
  440. function receiveUpdate() {
  441. beforeEach(async function (ctx) {
  442. await ctx.TpdsUpdateHandler.promises.newUpdate(
  443. ctx.userId,
  444. '', // projectId
  445. ctx.projectName,
  446. ctx.path,
  447. ctx.update,
  448. ctx.source
  449. )
  450. })
  451. }
  452. function receiveUpdateById() {
  453. beforeEach(async function (ctx) {
  454. await ctx.TpdsUpdateHandler.promises.newUpdate(
  455. ctx.userId,
  456. ctx.projectId,
  457. '', // projectName
  458. ctx.path,
  459. ctx.update,
  460. ctx.source
  461. )
  462. })
  463. }
  464. function receiveFileDelete() {
  465. beforeEach(async function (ctx) {
  466. await ctx.TpdsUpdateHandler.promises.deleteUpdate(
  467. ctx.userId,
  468. '', // projectId
  469. ctx.projectName,
  470. ctx.path,
  471. ctx.source
  472. )
  473. })
  474. }
  475. function receiveFileDeleteById() {
  476. beforeEach(async function (ctx) {
  477. await ctx.TpdsUpdateHandler.promises.deleteUpdate(
  478. ctx.userId,
  479. ctx.projectId,
  480. '', // projectName
  481. ctx.path,
  482. ctx.source
  483. )
  484. })
  485. }
  486. function receiveProjectDelete() {
  487. beforeEach(async function (ctx) {
  488. await ctx.TpdsUpdateHandler.promises.deleteUpdate(
  489. ctx.userId,
  490. '', // projectId
  491. ctx.projectName,
  492. '/',
  493. ctx.source
  494. )
  495. })
  496. }
  497. function receiveFolderUpdate() {
  498. beforeEach(async function (ctx) {
  499. await ctx.TpdsUpdateHandler.promises.createFolder(
  500. ctx.userId,
  501. ctx.projectId,
  502. ctx.projectName,
  503. ctx.folderPath
  504. )
  505. })
  506. }
  507. /* Expectations */
  508. function expectProjectCreated() {
  509. it('creates a project', function (ctx) {
  510. expect(
  511. ctx.ProjectCreationHandler.promises.createBlankProject
  512. ).to.have.been.calledWith(ctx.userId, ctx.projectName)
  513. })
  514. it('sets the root doc', function (ctx) {
  515. expect(
  516. ctx.RootDocManager.setRootDocAutomaticallyInBackground
  517. ).to.have.been.calledWith(ctx.projects.active1._id)
  518. })
  519. }
  520. function expectProjectNotCreated() {
  521. it('does not create a project', function (ctx) {
  522. expect(ctx.ProjectCreationHandler.promises.createBlankProject).not.to.have
  523. .been.called
  524. })
  525. it('does not set the root doc', function (ctx) {
  526. expect(ctx.RootDocManager.setRootDocAutomaticallyInBackground).not.to.have
  527. .been.called
  528. })
  529. }
  530. function expectUpdateProcessed() {
  531. it('processes the update', function (ctx) {
  532. expect(ctx.UpdateMerger.promises.mergeUpdate).to.have.been.calledWith(
  533. ctx.userId,
  534. ctx.projects.active1._id,
  535. ctx.path,
  536. ctx.update,
  537. ctx.source
  538. )
  539. })
  540. }
  541. function expectUpdateNotProcessed() {
  542. it('does not process the update', function (ctx) {
  543. expect(ctx.UpdateMerger.promises.mergeUpdate).not.to.have.been.called
  544. })
  545. }
  546. function expectFolderUpdateProcessed() {
  547. it('processes the folder update', function (ctx) {
  548. expect(ctx.UpdateMerger.promises.createFolder).to.have.been.calledWith(
  549. ctx.projects.active1._id,
  550. ctx.folderPath,
  551. ctx.userId
  552. )
  553. })
  554. }
  555. function expectFolderUpdateNotProcessed() {
  556. it("doesn't process the folder update", function (ctx) {
  557. expect(ctx.UpdateMerger.promises.createFolder).not.to.have.been.called
  558. })
  559. }
  560. function expectDropboxUnlinked() {
  561. it('unlinks Dropbox', function (ctx) {
  562. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  563. 'removeDropbox',
  564. ctx.userId,
  565. 'duplicate-projects'
  566. )
  567. })
  568. it('creates a notification that dropbox was unlinked', function (ctx) {
  569. expect(
  570. ctx.NotificationsBuilder.promises.dropboxDuplicateProjectNames
  571. ).to.have.been.calledWith(ctx.userId)
  572. expect(ctx.notification.create).to.have.been.calledWith(ctx.projectName)
  573. })
  574. }
  575. function expectDropboxNotUnlinked() {
  576. it('does not unlink Dropbox', function (ctx) {
  577. expect(ctx.Modules.promises.hooks.fire).not.to.have.been.called
  578. })
  579. it('does not create a notification that dropbox was unlinked', function (ctx) {
  580. expect(ctx.NotificationsBuilder.promises.dropboxDuplicateProjectNames).not
  581. .to.have.been.called
  582. })
  583. }
  584. function expectDeleteProcessed() {
  585. it('processes the delete', function (ctx) {
  586. expect(ctx.UpdateMerger.promises.deleteUpdate).to.have.been.calledWith(
  587. ctx.userId,
  588. ctx.projects.active1._id,
  589. ctx.path,
  590. ctx.source
  591. )
  592. })
  593. }
  594. function expectDeleteNotProcessed() {
  595. it('does not process the delete', function (ctx) {
  596. expect(ctx.UpdateMerger.promises.deleteUpdate).not.to.have.been.called
  597. })
  598. }
  599. function expectProjectDeleted() {
  600. it('deletes the project', function (ctx) {
  601. expect(
  602. ctx.ProjectDeleter.promises.markAsDeletedByExternalSource
  603. ).to.have.been.calledWith(ctx.projects.active1._id)
  604. })
  605. }
  606. function expectProjectNotDeleted() {
  607. it('does not delete the project', function (ctx) {
  608. expect(ctx.ProjectDeleter.promises.markAsDeletedByExternalSource).not.to
  609. .have.been.called
  610. })
  611. }