TpdsUpdateHandlerTests.mjs 17 KB

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