TpdsUpdateHandlerTests.js 17 KB

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