const SandboxedModule = require('sandboxed-module') const sinon = require('sinon') const { expect } = require('chai') const { ObjectId } = require('mongodb') const Errors = require('../../../../app/src/Features/Errors/Errors') const MODULE_PATH = '../../../../app/src/Features/ThirdPartyDataStore/TpdsUpdateHandler.js' describe('TpdsUpdateHandler', function() { beforeEach(function() { this.clock = sinon.useFakeTimers() }) afterEach(function() { this.clock.restore() }) beforeEach(function() { this.projectName = 'My recipes' this.projects = { active1: { _id: new ObjectId(), name: this.projectName }, active2: { _id: new ObjectId(), name: this.projectName }, archived1: { _id: new ObjectId(), name: this.projectName, archived: [this.userId] }, archived2: { _id: new ObjectId(), name: this.projectName, archived: [this.userId] } } this.userId = new ObjectId() this.source = 'dropbox' this.path = `/some/file` this.update = {} this.CooldownManager = { isProjectOnCooldown: sinon.stub().yields(null, false) } this.FileTypeManager = { shouldIgnore: sinon.stub().yields(null, false) } this.Modules = { hooks: { fire: sinon.stub().yields() } } this.notification = { create: sinon.stub().yields() } this.NotificationsBuilder = { dropboxDuplicateProjectNames: sinon.stub().returns(this.notification) } this.ProjectCreationHandler = { createBlankProject: sinon.stub().yields(null, this.projects.active1) } this.ProjectDeleter = { markAsDeletedByExternalSource: sinon.stub().yields() } this.ProjectGetter = { findUsersProjectsByName: sinon.stub() } this.ProjectHelper = { isArchivedOrTrashed: sinon.stub().returns(false) } this.ProjectHelper.isArchivedOrTrashed .withArgs(this.projects.archived1, this.userId) .returns(true) this.ProjectHelper.isArchivedOrTrashed .withArgs(this.projects.archived2, this.userId) .returns(true) this.RootDocManager = { setRootDocAutomatically: sinon.stub() } this.UpdateMerger = { deleteUpdate: sinon.stub().yields(), mergeUpdate: sinon.stub().yields() } this.TpdsUpdateHandler = SandboxedModule.require(MODULE_PATH, { requires: { '../Cooldown/CooldownManager': this.CooldownManager, '../Uploads/FileTypeManager': this.FileTypeManager, '../../infrastructure/Modules': this.Modules, '../Notifications/NotificationsBuilder': this.NotificationsBuilder, '../Project/ProjectCreationHandler': this.ProjectCreationHandler, '../Project/ProjectDeleter': this.ProjectDeleter, '../Project/ProjectGetter': this.ProjectGetter, '../Project/ProjectHelper': this.ProjectHelper, '../Project/ProjectRootDocManager': this.RootDocManager, './UpdateMerger': this.UpdateMerger } }) }) describe('getting an update', function() { describe('with no matching project', function() { setupMatchingProjects([]) receiveUpdate() expectProjectCreated() expectUpdateProcessed() }) describe('with one matching active project', function() { setupMatchingProjects(['active1']) receiveUpdate() expectProjectNotCreated() expectUpdateProcessed() }) describe('with one matching archived project', function() { setupMatchingProjects(['archived1']) receiveUpdate() expectProjectNotCreated() expectUpdateNotProcessed() expectDropboxNotUnlinked() }) describe('with two matching active projects', function() { setupMatchingProjects(['active1', 'active2']) receiveUpdate() expectProjectNotCreated() expectUpdateNotProcessed() expectDropboxUnlinked() }) describe('with two matching archived projects', function() { setupMatchingProjects(['archived1', 'archived2']) receiveUpdate() expectProjectNotCreated() expectUpdateNotProcessed() expectDropboxNotUnlinked() }) describe('with one matching active and one matching archived project', function() { setupMatchingProjects(['active1', 'archived1']) receiveUpdate() expectProjectNotCreated() expectUpdateNotProcessed() expectDropboxUnlinked() }) describe('update to a file that should be ignored', function(done) { setupMatchingProjects(['active1']) beforeEach(function() { this.FileTypeManager.shouldIgnore.yields(null, true) }) receiveUpdate() expectProjectNotCreated() expectUpdateNotProcessed() expectDropboxNotUnlinked() }) describe('update to a project on cooldown', function(done) { setupMatchingProjects(['active1']) setupProjectOnCooldown() beforeEach(function(done) { this.TpdsUpdateHandler.newUpdate( this.userId, this.projectName, this.path, this.update, this.source, err => { expect(err).to.be.instanceof(Errors.TooManyRequestsError) done() } ) }) expectUpdateNotProcessed() }) }) describe('getting a file delete', function() { describe('with no matching project', function() { setupMatchingProjects([]) receiveFileDelete() expectDeleteNotProcessed() expectProjectNotDeleted() }) describe('with one matching active project', function() { setupMatchingProjects(['active1']) receiveFileDelete() expectDeleteProcessed() expectProjectNotDeleted() }) describe('with one matching archived project', function() { setupMatchingProjects(['archived1']) receiveFileDelete() expectDeleteNotProcessed() expectProjectNotDeleted() }) describe('with two matching active projects', function() { setupMatchingProjects(['active1', 'active2']) receiveFileDelete() expectDeleteNotProcessed() expectProjectNotDeleted() expectDropboxUnlinked() }) describe('with two matching archived projects', function() { setupMatchingProjects(['archived1', 'archived2']) receiveFileDelete() expectDeleteNotProcessed() expectProjectNotDeleted() expectDropboxNotUnlinked() }) describe('with one matching active and one matching archived project', function() { setupMatchingProjects(['active1', 'archived1']) receiveFileDelete() expectDeleteNotProcessed() expectProjectNotDeleted() expectDropboxUnlinked() }) }) describe('getting a project delete', function() { describe('with no matching project', function() { setupMatchingProjects([]) receiveProjectDelete() expectDeleteNotProcessed() expectProjectNotDeleted() }) describe('with one matching active project', function() { setupMatchingProjects(['active1']) receiveProjectDelete() expectDeleteNotProcessed() expectProjectDeleted() }) describe('with one matching archived project', function() { setupMatchingProjects(['archived1']) receiveProjectDelete() expectDeleteNotProcessed() expectProjectNotDeleted() }) describe('with two matching active projects', function() { setupMatchingProjects(['active1', 'active2']) receiveProjectDelete() expectDeleteNotProcessed() expectProjectNotDeleted() expectDropboxUnlinked() }) describe('with two matching archived projects', function() { setupMatchingProjects(['archived1', 'archived2']) receiveProjectDelete() expectDeleteNotProcessed() expectProjectNotDeleted() expectDropboxNotUnlinked() }) describe('with one matching active and one matching archived project', function() { setupMatchingProjects(['active1', 'archived1']) receiveProjectDelete() expectDeleteNotProcessed() expectProjectNotDeleted() expectDropboxUnlinked() }) }) }) /* Setup helpers */ function setupMatchingProjects(projectKeys) { beforeEach(function() { const projects = projectKeys.map(key => this.projects[key]) this.ProjectGetter.findUsersProjectsByName .withArgs(this.userId, this.projectName) .yields(null, projects) }) } function setupProjectOnCooldown() { beforeEach(function() { this.CooldownManager.isProjectOnCooldown .withArgs(this.projects.active1._id) .yields(null, true) }) } /* Test helpers */ function receiveUpdate() { beforeEach(function(done) { this.TpdsUpdateHandler.newUpdate( this.userId, this.projectName, this.path, this.update, this.source, done ) }) } function receiveFileDelete() { beforeEach(function(done) { this.TpdsUpdateHandler.deleteUpdate( this.userId, this.projectName, this.path, this.source, done ) }) } function receiveProjectDelete() { beforeEach(function(done) { this.TpdsUpdateHandler.deleteUpdate( this.userId, this.projectName, '/', this.source, done ) }) } /* Expectations */ function expectProjectCreated() { it('creates a project', function() { expect( this.ProjectCreationHandler.createBlankProject ).to.have.been.calledWith(this.userId, this.projectName) }) it('sets the root doc', function() { // Fire pending timers this.clock.runAll() expect(this.RootDocManager.setRootDocAutomatically).to.have.been.calledWith( this.projects.active1._id ) }) } function expectProjectNotCreated() { it('does not create a project', function() { expect(this.ProjectCreationHandler.createBlankProject).not.to.have.been .called }) it('does not set the root doc', function() { // Fire pending timers this.clock.runAll() expect(this.RootDocManager.setRootDocAutomatically).not.to.have.been.called }) } function expectUpdateProcessed() { it('processes the update', function() { expect(this.UpdateMerger.mergeUpdate).to.have.been.calledWith( this.userId, this.projects.active1._id, this.path, this.update, this.source ) }) } function expectUpdateNotProcessed() { it('does not process the update', function() { expect(this.UpdateMerger.mergeUpdate).not.to.have.been.called }) } function expectDropboxUnlinked() { it('unlinks Dropbox', function() { expect(this.Modules.hooks.fire).to.have.been.calledWith( 'removeDropbox', this.userId ) }) it('creates a notification that dropbox was unlinked', function() { expect( this.NotificationsBuilder.dropboxDuplicateProjectNames ).to.have.been.calledWith(this.userId) expect(this.notification.create).to.have.been.calledWith(this.projectName) }) } function expectDropboxNotUnlinked() { it('does not unlink Dropbox', function() { expect(this.Modules.hooks.fire).not.to.have.been.called }) it('does not create a notification that dropbox was unlinked', function() { expect(this.NotificationsBuilder.dropboxDuplicateProjectNames).not.to.have .been.called }) } function expectDeleteProcessed() { it('processes the delete', function() { expect(this.UpdateMerger.deleteUpdate).to.have.been.calledWith( this.userId, this.projects.active1._id, this.path, this.source ) }) } function expectDeleteNotProcessed() { it('does not process the delete', function() { expect(this.UpdateMerger.deleteUpdate).not.to.have.been.called }) } function expectProjectDeleted() { it('deletes the project', function() { expect( this.ProjectDeleter.markAsDeletedByExternalSource ).to.have.been.calledWith(this.projects.active1._id) }) } function expectProjectNotDeleted() { it('does not delete the project', function() { expect(this.ProjectDeleter.markAsDeletedByExternalSource).not.to.have.been .called }) }