diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js index 1ea71176a3..a20cbd9e68 100644 --- a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.js @@ -82,6 +82,7 @@ module.exports = LinkedFilesController = { } data.provider = provider + data.importedAt = new Date().toISOString() return Agent.createLinkedFile( projectId, @@ -136,6 +137,8 @@ module.exports = LinkedFilesController = { return res.sendStatus(400) } + linkedFileData.importedAt = new Date().toISOString() + Agent.refreshLinkedFile( projectId, linkedFileData, diff --git a/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js b/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js index 612a61307e..3c0272da7b 100644 --- a/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js +++ b/services/web/app/src/Features/LinkedFiles/ProjectFileAgent.js @@ -215,7 +215,8 @@ module.exports = ProjectFileAgent = { 'provider', 'source_project_id', 'v1_source_doc_id', - 'source_entity_path' + 'source_entity_path', + 'importedAt' ) }, diff --git a/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js b/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js index 15cb769c97..c6533b916f 100644 --- a/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js +++ b/services/web/app/src/Features/LinkedFiles/ProjectOutputFileAgent.js @@ -110,6 +110,7 @@ function _sanitizeData(data) { source_output_file_path: data.source_output_file_path, build_id: data.build_id, clsiServerId: data.clsiServerId, + importedAt: data.importedAt, } } diff --git a/services/web/app/src/Features/LinkedFiles/UrlAgent.js b/services/web/app/src/Features/LinkedFiles/UrlAgent.js index 3af360e2e7..f35a2395ab 100644 --- a/services/web/app/src/Features/LinkedFiles/UrlAgent.js +++ b/services/web/app/src/Features/LinkedFiles/UrlAgent.js @@ -63,6 +63,7 @@ function _sanitizeData(data) { return { provider: data.provider, url: UrlHelper.prependHttpIfNeeded(data.url), + importedAt: data.importedAt, } } diff --git a/services/web/test/acceptance/src/LinkedFilesTests.js b/services/web/test/acceptance/src/LinkedFilesTests.js index c111035ee9..8185d66eeb 100644 --- a/services/web/test/acceptance/src/LinkedFilesTests.js +++ b/services/web/test/acceptance/src/LinkedFilesTests.js @@ -1,6 +1,7 @@ const { expect } = require('chai') const _ = require('lodash') const fs = require('fs') +const timekeeper = require('timekeeper') const Settings = require('@overleaf/settings') const User = require('./helpers/User').promises @@ -23,6 +24,14 @@ LinkedUrlProxy.get('/', (req, res, next) => { }) describe('LinkedFiles', function () { + before(function () { + timekeeper.freeze(new Date()) + }) + + after(function () { + timekeeper.reset() + }) + let projectOne, projectOneId, projectOneRootFolderId let projectTwo, projectTwoId, projectTwoRootFolderId const sourceDocName = 'test.txt' @@ -129,6 +138,7 @@ describe('LinkedFiles', function () { provider: 'project_file', source_project_id: projectTwoId, source_entity_path: `/${sourceDocName}`, + importedAt: new Date().toISOString(), }) expect(firstFile.name).to.equal('test-link.txt') @@ -264,6 +274,7 @@ describe('LinkedFiles', function () { expect(file.linkedFileData).to.deep.equal({ provider: 'url', url: 'http://example.com/foo', + importedAt: new Date().toISOString(), }) ;({ response, body } = await owner.doRequest( 'get', @@ -303,6 +314,7 @@ describe('LinkedFiles', function () { expect(file.linkedFileData).to.deep.equal({ provider: 'url', url: 'http://example.com/bar', + importedAt: new Date().toISOString(), }) ;({ response, body } = await owner.doRequest( 'get', @@ -405,6 +417,7 @@ describe('LinkedFiles', function () { expect(file.linkedFileData).to.deep.equal({ provider: 'url', url: 'http://example.com/foo', + importedAt: new Date().toISOString(), }) ;({ response, body } = await owner.doRequest( 'get', @@ -460,6 +473,7 @@ describe('LinkedFiles', function () { source_project_id: projectTwoId, source_output_file_path: 'project.pdf', build_id: '1234-abcd', + importedAt: new Date().toISOString(), }) expect(firstFile.name).to.equal('test.pdf') diff --git a/services/web/test/unit/src/LinkedFiles/LinkedFilesControllerTests.js b/services/web/test/unit/src/LinkedFiles/LinkedFilesControllerTests.js new file mode 100644 index 0000000000..f1f31b8757 --- /dev/null +++ b/services/web/test/unit/src/LinkedFiles/LinkedFilesControllerTests.js @@ -0,0 +1,139 @@ +const { expect } = require('chai') +const SandboxedModule = require('sandboxed-module') +const sinon = require('sinon') +const modulePath = + '../../../../app/src/Features/LinkedFiles/LinkedFilesController' + +describe('LinkedFilesController', function () { + beforeEach(function () { + this.fakeTime = new Date() + this.clock = sinon.useFakeTimers(this.fakeTime.getTime()) + }) + + afterEach(function () { + this.clock.restore() + }) + + beforeEach(function () { + this.userId = 'user-id' + this.Agent = { + createLinkedFile: sinon.stub().yields(), + refreshLinkedFile: sinon.stub().yields(), + } + this.projectId = 'projectId' + this.provider = 'provider' + this.name = 'linked-file-name' + this.data = { customAgentData: 'foo' } + this.LinkedFilesHandler = { + getFileById: sinon.stub(), + } + this.AnalyticsManager = {} + this.SessionManager = { + getLoggedInUserId: sinon.stub().returns(this.userId), + } + this.EditorRealTimeController = {} + this.ReferencesHandler = {} + this.UrlAgent = {} + this.ProjectFileAgent = {} + this.ProjectOutputFileAgent = {} + this.EditorController = {} + this.ProjectLocator = {} + this.logger = {} + this.settings = { enabledLinkedFileTypes: [] } + this.LinkedFilesController = SandboxedModule.require(modulePath, { + requires: { + '../Authentication/SessionManager': this.SessionManager, + '../../../../app/src/Features/Analytics/AnalyticsManager': + this.AnalyticsManager, + './LinkedFilesHandler': this.LinkedFilesHandler, + '../Editor/EditorRealTimeController': this.EditorRealTimeController, + '../References/ReferencesHandler': this.ReferencesHandler, + './UrlAgent': this.UrlAgent, + './ProjectFileAgent': this.ProjectFileAgent, + './ProjectOutputFileAgent': this.ProjectOutputFileAgent, + '../Editor/EditorController': this.EditorController, + '../Project/ProjectLocator': this.ProjectLocator, + '@overleaf/logger': this.logger, + '@overleaf/settings': this.settings, + }, + }) + this.LinkedFilesController._getAgent = sinon.stub().returns(this.Agent) + }) + + describe('createLinkedFile', function () { + beforeEach(function () { + this.req = { + params: { project_id: this.projectId }, + body: { + name: this.name, + provider: this.provider, + data: this.data, + }, + } + this.next = sinon.stub() + }) + + it('sets importedAt timestamp on linkedFileData', function (done) { + this.next = sinon.stub().callsFake(() => done('unexpected error')) + this.res = { + json: () => { + expect(this.Agent.createLinkedFile).to.have.been.calledWith( + this.projectId, + { ...this.data, importedAt: this.fakeTime.toISOString() }, + this.name, + undefined, + this.userId + ) + done() + }, + } + this.LinkedFilesController.createLinkedFile(this.req, this.res, this.next) + }) + }) + describe('refreshLinkedFiles', function () { + beforeEach(function () { + this.data.provider = this.provider + this.file = { + name: this.name, + linkedFileData: { + ...this.data, + importedAt: new Date(2020, 1, 1).toISOString(), + }, + } + this.LinkedFilesHandler.getFileById + .withArgs(this.projectId, 'file-id') + .yields(null, this.file, 'fake-path', { + _id: 'parent-folder-id', + }) + this.req = { + params: { project_id: this.projectId, file_id: 'file-id' }, + body: {}, + } + this.next = sinon.stub() + }) + + it('resets importedAt timestamp on linkedFileData', function (done) { + this.next = sinon.stub().callsFake(() => done('unexpected error')) + this.res = { + json: () => { + expect(this.Agent.refreshLinkedFile).to.have.been.calledWith( + this.projectId, + { + ...this.data, + importedAt: this.fakeTime.toISOString(), + }, + this.name, + 'parent-folder-id', + this.userId + ) + done() + }, + } + this.LinkedFilesController.refreshLinkedFile( + this.req, + this.res, + this.next + ) + }) + }) +})