decaffeinate: rename test/unit/coffee to test/unit/js
This commit is contained in:
275
test/unit/js/CompileControllerTests.js
Normal file
275
test/unit/js/CompileControllerTests.js
Normal file
@@ -0,0 +1,275 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/CompileController');
|
||||
const tk = require("timekeeper");
|
||||
|
||||
describe("CompileController", function() {
|
||||
beforeEach(function() {
|
||||
this.CompileController = SandboxedModule.require(modulePath, { requires: {
|
||||
"./CompileManager": (this.CompileManager = {}),
|
||||
"./RequestParser": (this.RequestParser = {}),
|
||||
"settings-sharelatex": (this.Settings = {
|
||||
apis: {
|
||||
clsi: {
|
||||
url: "http://clsi.example.com"
|
||||
}
|
||||
}
|
||||
}),
|
||||
"./ProjectPersistenceManager": (this.ProjectPersistenceManager = {}),
|
||||
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), err:sinon.stub(), warn: sinon.stub()})
|
||||
}
|
||||
});
|
||||
this.Settings.externalUrl = "http://www.example.com";
|
||||
this.req = {};
|
||||
this.res = {};
|
||||
return this.next = sinon.stub();
|
||||
});
|
||||
|
||||
describe("compile", function() {
|
||||
beforeEach(function() {
|
||||
this.req.body = {
|
||||
compile: "mock-body"
|
||||
};
|
||||
this.req.params =
|
||||
{project_id: (this.project_id = "project-id-123")};
|
||||
this.request = {
|
||||
compile: "mock-parsed-request"
|
||||
};
|
||||
this.request_with_project_id = {
|
||||
compile: this.request.compile,
|
||||
project_id: this.project_id
|
||||
};
|
||||
this.output_files = [{
|
||||
path: "output.pdf",
|
||||
type: "pdf",
|
||||
build: 1234
|
||||
}, {
|
||||
path: "output.log",
|
||||
type: "log",
|
||||
build: 1234
|
||||
}];
|
||||
this.RequestParser.parse = sinon.stub().callsArgWith(1, null, this.request);
|
||||
this.ProjectPersistenceManager.markProjectAsJustAccessed = sinon.stub().callsArg(1);
|
||||
this.res.status = sinon.stub().returnsThis();
|
||||
return this.res.send = sinon.stub();
|
||||
});
|
||||
|
||||
describe("successfully", function() {
|
||||
beforeEach(function() {
|
||||
this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, this.output_files);
|
||||
return this.CompileController.compile(this.req, this.res);
|
||||
});
|
||||
|
||||
it("should parse the request", function() {
|
||||
return this.RequestParser.parse
|
||||
.calledWith(this.req.body)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should run the compile for the specified project", function() {
|
||||
return this.CompileManager.doCompileWithLock
|
||||
.calledWith(this.request_with_project_id)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should mark the project as accessed", function() {
|
||||
return this.ProjectPersistenceManager.markProjectAsJustAccessed
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should return the JSON response", function() {
|
||||
this.res.status.calledWith(200).should.equal(true);
|
||||
return this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: "success",
|
||||
error: null,
|
||||
outputFiles: this.output_files.map(file => {
|
||||
return {
|
||||
url: `${this.Settings.apis.clsi.url}/project/${this.project_id}/build/${file.build}/output/${file.path}`,
|
||||
path: file.path,
|
||||
type: file.type,
|
||||
build: file.build
|
||||
};
|
||||
})
|
||||
}
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with an error", function() {
|
||||
beforeEach(function() {
|
||||
this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, new Error(this.message = "error message"), null);
|
||||
return this.CompileController.compile(this.req, this.res);
|
||||
});
|
||||
|
||||
return it("should return the JSON response with the error", function() {
|
||||
this.res.status.calledWith(500).should.equal(true);
|
||||
return this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: "error",
|
||||
error: this.message,
|
||||
outputFiles: []
|
||||
}
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the request times out", function() {
|
||||
beforeEach(function() {
|
||||
this.error = new Error(this.message = "container timed out");
|
||||
this.error.timedout = true;
|
||||
this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, this.error, null);
|
||||
return this.CompileController.compile(this.req, this.res);
|
||||
});
|
||||
|
||||
return it("should return the JSON response with the timeout status", function() {
|
||||
this.res.status.calledWith(200).should.equal(true);
|
||||
return this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
status: "timedout",
|
||||
error: this.message,
|
||||
outputFiles: []
|
||||
}
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the request returns no output files", function() {
|
||||
beforeEach(function() {
|
||||
this.CompileManager.doCompileWithLock = sinon.stub().callsArgWith(1, null, []);
|
||||
return this.CompileController.compile(this.req, this.res);
|
||||
});
|
||||
|
||||
return it("should return the JSON response with the failure status", function() {
|
||||
this.res.status.calledWith(200).should.equal(true);
|
||||
return this.res.send
|
||||
.calledWith({
|
||||
compile: {
|
||||
error: null,
|
||||
status: "failure",
|
||||
outputFiles: []
|
||||
}
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncFromCode", function() {
|
||||
beforeEach(function() {
|
||||
this.file = "main.tex";
|
||||
this.line = 42;
|
||||
this.column = 5;
|
||||
this.project_id = "mock-project-id";
|
||||
this.req.params =
|
||||
{project_id: this.project_id};
|
||||
this.req.query = {
|
||||
file: this.file,
|
||||
line: this.line.toString(),
|
||||
column: this.column.toString()
|
||||
};
|
||||
this.res.json = sinon.stub();
|
||||
|
||||
this.CompileManager.syncFromCode = sinon.stub().callsArgWith(5, null, (this.pdfPositions = ["mock-positions"]));
|
||||
return this.CompileController.syncFromCode(this.req, this.res, this.next);
|
||||
});
|
||||
|
||||
it("should find the corresponding location in the PDF", function() {
|
||||
return this.CompileManager.syncFromCode
|
||||
.calledWith(this.project_id, undefined, this.file, this.line, this.column)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should return the positions", function() {
|
||||
return this.res.json
|
||||
.calledWith({
|
||||
pdf: this.pdfPositions
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncFromPdf", function() {
|
||||
beforeEach(function() {
|
||||
this.page = 5;
|
||||
this.h = 100.23;
|
||||
this.v = 45.67;
|
||||
this.project_id = "mock-project-id";
|
||||
this.req.params =
|
||||
{project_id: this.project_id};
|
||||
this.req.query = {
|
||||
page: this.page.toString(),
|
||||
h: this.h.toString(),
|
||||
v: this.v.toString()
|
||||
};
|
||||
this.res.json = sinon.stub();
|
||||
|
||||
this.CompileManager.syncFromPdf = sinon.stub().callsArgWith(5, null, (this.codePositions = ["mock-positions"]));
|
||||
return this.CompileController.syncFromPdf(this.req, this.res, this.next);
|
||||
});
|
||||
|
||||
it("should find the corresponding location in the code", function() {
|
||||
return this.CompileManager.syncFromPdf
|
||||
.calledWith(this.project_id, undefined, this.page, this.h, this.v)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should return the positions", function() {
|
||||
return this.res.json
|
||||
.calledWith({
|
||||
code: this.codePositions
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("wordcount", function() {
|
||||
beforeEach(function() {
|
||||
this.file = "main.tex";
|
||||
this.project_id = "mock-project-id";
|
||||
this.req.params =
|
||||
{project_id: this.project_id};
|
||||
this.req.query = {
|
||||
file: this.file,
|
||||
image: (this.image = "example.com/image")
|
||||
};
|
||||
this.res.json = sinon.stub();
|
||||
|
||||
this.CompileManager.wordcount = sinon.stub().callsArgWith(4, null, (this.texcount = ["mock-texcount"]));
|
||||
return this.CompileController.wordcount(this.req, this.res, this.next);
|
||||
});
|
||||
|
||||
it("should return the word count of a file", function() {
|
||||
return this.CompileManager.wordcount
|
||||
.calledWith(this.project_id, undefined, this.file, this.image)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should return the texcount info", function() {
|
||||
return this.res.json
|
||||
.calledWith({
|
||||
texcount: this.texcount
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
435
test/unit/js/CompileManagerTests.js
Normal file
435
test/unit/js/CompileManagerTests.js
Normal file
@@ -0,0 +1,435 @@
|
||||
/* eslint-disable
|
||||
camelcase,
|
||||
chai-friendly/no-unused-expressions,
|
||||
no-path-concat,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/CompileManager');
|
||||
const tk = require("timekeeper");
|
||||
const { EventEmitter } = require("events");
|
||||
const Path = require("path");
|
||||
|
||||
describe("CompileManager", function() {
|
||||
beforeEach(function() {
|
||||
this.CompileManager = SandboxedModule.require(modulePath, { requires: {
|
||||
"./LatexRunner": (this.LatexRunner = {}),
|
||||
"./ResourceWriter": (this.ResourceWriter = {}),
|
||||
"./OutputFileFinder": (this.OutputFileFinder = {}),
|
||||
"./OutputCacheManager": (this.OutputCacheManager = {}),
|
||||
"settings-sharelatex": (this.Settings = {
|
||||
path: {
|
||||
compilesDir: "/compiles/dir"
|
||||
},
|
||||
synctexBaseDir() { return "/compile"; },
|
||||
clsi: {
|
||||
docker: {
|
||||
image: "SOMEIMAGE"
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
"logger-sharelatex": (this.logger = { log: sinon.stub() , info() {}}),
|
||||
"child_process": (this.child_process = {}),
|
||||
"./CommandRunner": (this.CommandRunner = {}),
|
||||
"./DraftModeManager": (this.DraftModeManager = {}),
|
||||
"./TikzManager": (this.TikzManager = {}),
|
||||
"./LockManager": (this.LockManager = {}),
|
||||
"fs": (this.fs = {}),
|
||||
"fs-extra": (this.fse = { ensureDir: sinon.stub().callsArg(1) })
|
||||
}
|
||||
});
|
||||
this.callback = sinon.stub();
|
||||
this.project_id = "project-id-123";
|
||||
return this.user_id = "1234";
|
||||
});
|
||||
describe("doCompileWithLock", function() {
|
||||
beforeEach(function() {
|
||||
this.request = {
|
||||
resources: (this.resources = "mock-resources"),
|
||||
project_id: this.project_id,
|
||||
user_id: this.user_id
|
||||
};
|
||||
this.output_files = ["foo", "bar"];
|
||||
this.Settings.compileDir = "compiles";
|
||||
this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`;
|
||||
this.CompileManager.doCompile = sinon.stub().callsArgWith(1, null, this.output_files);
|
||||
return this.LockManager.runWithLock = (lockFile, runner, callback) =>
|
||||
runner((err, ...result) => callback(err, ...Array.from(result)))
|
||||
;
|
||||
});
|
||||
|
||||
describe("when the project is not locked", function() {
|
||||
beforeEach(function() {
|
||||
return this.CompileManager.doCompileWithLock(this.request, this.callback);
|
||||
});
|
||||
|
||||
it("should ensure that the compile directory exists", function() {
|
||||
return this.fse.ensureDir.calledWith(this.compileDir)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should call doCompile with the request", function() {
|
||||
return this.CompileManager.doCompile
|
||||
.calledWith(this.request)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with the output files", function() {
|
||||
return this.callback.calledWithExactly(null, this.output_files)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the project is locked", function() {
|
||||
beforeEach(function() {
|
||||
this.error = new Error("locked");
|
||||
this.LockManager.runWithLock = (lockFile, runner, callback) => {
|
||||
return callback(this.error);
|
||||
};
|
||||
return this.CompileManager.doCompileWithLock(this.request, this.callback);
|
||||
});
|
||||
|
||||
it("should ensure that the compile directory exists", function() {
|
||||
return this.fse.ensureDir.calledWith(this.compileDir)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should not call doCompile with the request", function() {
|
||||
return this.CompileManager.doCompile
|
||||
.called.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should call the callback with the error", function() {
|
||||
return this.callback.calledWithExactly(this.error)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("doCompile", function() {
|
||||
beforeEach(function() {
|
||||
this.output_files = [{
|
||||
path: "output.log",
|
||||
type: "log"
|
||||
}, {
|
||||
path: "output.pdf",
|
||||
type: "pdf"
|
||||
}];
|
||||
this.build_files = [{
|
||||
path: "output.log",
|
||||
type: "log",
|
||||
build: 1234
|
||||
}, {
|
||||
path: "output.pdf",
|
||||
type: "pdf",
|
||||
build: 1234
|
||||
}];
|
||||
this.request = {
|
||||
resources: (this.resources = "mock-resources"),
|
||||
rootResourcePath: (this.rootResourcePath = "main.tex"),
|
||||
project_id: this.project_id,
|
||||
user_id: this.user_id,
|
||||
compiler: (this.compiler = "pdflatex"),
|
||||
timeout: (this.timeout = 42000),
|
||||
imageName: (this.image = "example.com/image"),
|
||||
flags: (this.flags = ["-file-line-error"])
|
||||
};
|
||||
this.env = {};
|
||||
this.Settings.compileDir = "compiles";
|
||||
this.compileDir = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`;
|
||||
this.ResourceWriter.syncResourcesToDisk = sinon.stub().callsArgWith(2, null, this.resources);
|
||||
this.LatexRunner.runLatex = sinon.stub().callsArg(2);
|
||||
this.OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, this.output_files);
|
||||
this.OutputCacheManager.saveOutputFiles = sinon.stub().callsArgWith(2, null, this.build_files);
|
||||
this.DraftModeManager.injectDraftMode = sinon.stub().callsArg(1);
|
||||
return this.TikzManager.checkMainFile = sinon.stub().callsArg(3, false);
|
||||
});
|
||||
|
||||
describe("normally", function() {
|
||||
beforeEach(function() {
|
||||
return this.CompileManager.doCompile(this.request, this.callback);
|
||||
});
|
||||
|
||||
it("should write the resources to disk", function() {
|
||||
return this.ResourceWriter.syncResourcesToDisk
|
||||
.calledWith(this.request, this.compileDir)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should run LaTeX", function() {
|
||||
return this.LatexRunner.runLatex
|
||||
.calledWith(`${this.project_id}-${this.user_id}`, {
|
||||
directory: this.compileDir,
|
||||
mainFile: this.rootResourcePath,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: this.env
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should find the output files", function() {
|
||||
return this.OutputFileFinder.findOutputFiles
|
||||
.calledWith(this.resources, this.compileDir)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should return the output files", function() {
|
||||
return this.callback.calledWith(null, this.build_files).should.equal(true);
|
||||
});
|
||||
|
||||
return it("should not inject draft mode by default", function() {
|
||||
return this.DraftModeManager.injectDraftMode.called.should.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with draft mode", function() {
|
||||
beforeEach(function() {
|
||||
this.request.draft = true;
|
||||
return this.CompileManager.doCompile(this.request, this.callback);
|
||||
});
|
||||
|
||||
return it("should inject the draft mode header", function() {
|
||||
return this.DraftModeManager.injectDraftMode
|
||||
.calledWith(this.compileDir + "/" + this.rootResourcePath)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a check option", function() {
|
||||
beforeEach(function() {
|
||||
this.request.check = "error";
|
||||
return this.CompileManager.doCompile(this.request, this.callback);
|
||||
});
|
||||
|
||||
return it("should run chktex", function() {
|
||||
return this.LatexRunner.runLatex
|
||||
.calledWith(`${this.project_id}-${this.user_id}`, {
|
||||
directory: this.compileDir,
|
||||
mainFile: this.rootResourcePath,
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: {'CHKTEX_OPTIONS': '-nall -e9 -e10 -w15 -w16', 'CHKTEX_EXIT_ON_ERROR':1, 'CHKTEX_ULIMIT_OPTIONS': '-t 5 -v 64000'}
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with a knitr file and check options", function() {
|
||||
beforeEach(function() {
|
||||
this.request.rootResourcePath = "main.Rtex";
|
||||
this.request.check = "error";
|
||||
return this.CompileManager.doCompile(this.request, this.callback);
|
||||
});
|
||||
|
||||
return it("should not run chktex", function() {
|
||||
return this.LatexRunner.runLatex
|
||||
.calledWith(`${this.project_id}-${this.user_id}`, {
|
||||
directory: this.compileDir,
|
||||
mainFile: "main.Rtex",
|
||||
compiler: this.compiler,
|
||||
timeout: this.timeout,
|
||||
image: this.image,
|
||||
flags: this.flags,
|
||||
environment: this.env
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearProject", function() {
|
||||
describe("succesfully", function() {
|
||||
beforeEach(function() {
|
||||
this.Settings.compileDir = "compiles";
|
||||
this.fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory(){ return true; }});
|
||||
this.proc = new EventEmitter();
|
||||
this.proc.stdout = new EventEmitter();
|
||||
this.proc.stderr = new EventEmitter();
|
||||
this.child_process.spawn = sinon.stub().returns(this.proc);
|
||||
this.CompileManager.clearProject(this.project_id, this.user_id, this.callback);
|
||||
return this.proc.emit("close", 0);
|
||||
});
|
||||
|
||||
it("should remove the project directory", function() {
|
||||
return this.child_process.spawn
|
||||
.calledWith("rm", ["-r", `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`])
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with a non-success status code", function() {
|
||||
beforeEach(function() {
|
||||
this.Settings.compileDir = "compiles";
|
||||
this.fs.lstat = sinon.stub().callsArgWith(1, null,{isDirectory(){ return true; }});
|
||||
this.proc = new EventEmitter();
|
||||
this.proc.stdout = new EventEmitter();
|
||||
this.proc.stderr = new EventEmitter();
|
||||
this.child_process.spawn = sinon.stub().returns(this.proc);
|
||||
this.CompileManager.clearProject(this.project_id, this.user_id, this.callback);
|
||||
this.proc.stderr.emit("data", (this.error = "oops"));
|
||||
return this.proc.emit("close", 1);
|
||||
});
|
||||
|
||||
it("should remove the project directory", function() {
|
||||
return this.child_process.spawn
|
||||
.calledWith("rm", ["-r", `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`])
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with an error from the stderr", function() {
|
||||
this.callback
|
||||
.calledWith(new Error())
|
||||
.should.equal(true);
|
||||
|
||||
return this.callback.args[0][0].message.should.equal(`rm -r ${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id} failed: ${this.error}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncing", function() {
|
||||
beforeEach(function() {
|
||||
this.page = 1;
|
||||
this.h = 42.23;
|
||||
this.v = 87.56;
|
||||
this.width = 100.01;
|
||||
this.height = 234.56;
|
||||
this.line = 5;
|
||||
this.column = 3;
|
||||
this.file_name = "main.tex";
|
||||
this.child_process.execFile = sinon.stub();
|
||||
return this.Settings.path.synctexBaseDir = project_id => `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`;
|
||||
});
|
||||
|
||||
describe("syncFromCode", function() {
|
||||
beforeEach(function() {
|
||||
this.fs.stat = sinon.stub().callsArgWith(1, null,{isFile(){ return true; }});
|
||||
this.stdout = `NODE\t${this.page}\t${this.h}\t${this.v}\t${this.width}\t${this.height}\n`;
|
||||
this.CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:this.stdout});
|
||||
return this.CompileManager.syncFromCode(this.project_id, this.user_id, this.file_name, this.line, this.column, this.callback);
|
||||
});
|
||||
|
||||
it("should execute the synctex binary", function() {
|
||||
const bin_path = Path.resolve(__dirname + "/../../../bin/synctex");
|
||||
const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf`;
|
||||
const file_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}`;
|
||||
return this.CommandRunner.run
|
||||
.calledWith(
|
||||
`${this.project_id}-${this.user_id}`,
|
||||
['/opt/synctex', 'code', synctex_path, file_path, this.line, this.column],
|
||||
`${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`,
|
||||
this.Settings.clsi.docker.image,
|
||||
60000,
|
||||
{}
|
||||
).should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with the parsed output", function() {
|
||||
return this.callback
|
||||
.calledWith(null, [{
|
||||
page: this.page,
|
||||
h: this.h,
|
||||
v: this.v,
|
||||
height: this.height,
|
||||
width: this.width
|
||||
}])
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("syncFromPdf", function() {
|
||||
beforeEach(function() {
|
||||
this.fs.stat = sinon.stub().callsArgWith(1, null,{isFile(){ return true; }});
|
||||
this.stdout = `NODE\t${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/${this.file_name}\t${this.line}\t${this.column}\n`;
|
||||
this.CommandRunner.run = sinon.stub().callsArgWith(6, null, {stdout:this.stdout});
|
||||
return this.CompileManager.syncFromPdf(this.project_id, this.user_id, this.page, this.h, this.v, this.callback);
|
||||
});
|
||||
|
||||
it("should execute the synctex binary", function() {
|
||||
const bin_path = Path.resolve(__dirname + "/../../../bin/synctex");
|
||||
const synctex_path = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}/output.pdf`;
|
||||
return this.CommandRunner.run
|
||||
.calledWith(
|
||||
`${this.project_id}-${this.user_id}`,
|
||||
['/opt/synctex', "pdf", synctex_path, this.page, this.h, this.v],
|
||||
`${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`,
|
||||
this.Settings.clsi.docker.image,
|
||||
60000,
|
||||
{}).should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with the parsed output", function() {
|
||||
return this.callback
|
||||
.calledWith(null, [{
|
||||
file: this.file_name,
|
||||
line: this.line,
|
||||
column: this.column
|
||||
}])
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return describe("wordcount", function() {
|
||||
beforeEach(function() {
|
||||
this.CommandRunner.run = sinon.stub().callsArg(6);
|
||||
this.fs.readFile = sinon.stub().callsArgWith(2, null, (this.stdout = "Encoding: ascii\nWords in text: 2"));
|
||||
this.callback = sinon.stub();
|
||||
|
||||
this.project_id;
|
||||
this.timeout = 60 * 1000;
|
||||
this.file_name = "main.tex";
|
||||
this.Settings.path.compilesDir = "/local/compile/directory";
|
||||
this.image = "example.com/image";
|
||||
|
||||
return this.CompileManager.wordcount(this.project_id, this.user_id, this.file_name, this.image, this.callback);
|
||||
});
|
||||
|
||||
it("should run the texcount command", function() {
|
||||
this.directory = `${this.Settings.path.compilesDir}/${this.project_id}-${this.user_id}`;
|
||||
this.file_path = `$COMPILE_DIR/${this.file_name}`;
|
||||
this.command =[ "texcount", "-nocol", "-inc", this.file_path, `-out=${this.file_path}.wc`];
|
||||
|
||||
return this.CommandRunner.run
|
||||
.calledWith(`${this.project_id}-${this.user_id}`, this.command, this.directory, this.image, this.timeout, {})
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with the parsed output", function() {
|
||||
return this.callback
|
||||
.calledWith(null, {
|
||||
encode: "ascii",
|
||||
textWords: 2,
|
||||
headWords: 0,
|
||||
outside: 0,
|
||||
headers: 0,
|
||||
elements: 0,
|
||||
mathInline: 0,
|
||||
mathDisplay: 0,
|
||||
errors: 0,
|
||||
messages: ""
|
||||
})
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
82
test/unit/js/ContentTypeMapperTests.js
Normal file
82
test/unit/js/ContentTypeMapperTests.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable
|
||||
camelcase,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/ContentTypeMapper');
|
||||
|
||||
describe('ContentTypeMapper', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
return this.ContentTypeMapper = SandboxedModule.require(modulePath);
|
||||
});
|
||||
|
||||
return describe('map', function() {
|
||||
|
||||
it('should map .txt to text/plain', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.txt');
|
||||
return content_type.should.equal('text/plain');
|
||||
});
|
||||
|
||||
it('should map .csv to text/csv', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.csv');
|
||||
return content_type.should.equal('text/csv');
|
||||
});
|
||||
|
||||
it('should map .pdf to application/pdf', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.pdf');
|
||||
return content_type.should.equal('application/pdf');
|
||||
});
|
||||
|
||||
it('should fall back to octet-stream', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.unknown');
|
||||
return content_type.should.equal('application/octet-stream');
|
||||
});
|
||||
|
||||
describe('coercing web files to plain text', function() {
|
||||
|
||||
it('should map .js to plain text', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.js');
|
||||
return content_type.should.equal('text/plain');
|
||||
});
|
||||
|
||||
it('should map .html to plain text', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.html');
|
||||
return content_type.should.equal('text/plain');
|
||||
});
|
||||
|
||||
return it('should map .css to plain text', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.css');
|
||||
return content_type.should.equal('text/plain');
|
||||
});
|
||||
});
|
||||
|
||||
return describe('image files', function() {
|
||||
|
||||
it('should map .png to image/png', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.png');
|
||||
return content_type.should.equal('image/png');
|
||||
});
|
||||
|
||||
it('should map .jpeg to image/jpeg', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.jpeg');
|
||||
return content_type.should.equal('image/jpeg');
|
||||
});
|
||||
|
||||
return it('should map .svg to text/plain to protect against XSS (SVG can execute JS)', function() {
|
||||
const content_type = this.ContentTypeMapper.map('example.svg');
|
||||
return content_type.should.equal('text/plain');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
193
test/unit/js/DockerLockManagerTests.js
Normal file
193
test/unit/js/DockerLockManagerTests.js
Normal file
@@ -0,0 +1,193 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
require("coffee-script");
|
||||
const modulePath = require('path').join(__dirname, '../../../app/coffee/DockerLockManager');
|
||||
|
||||
describe("LockManager", function() {
|
||||
beforeEach(function() {
|
||||
return this.LockManager = SandboxedModule.require(modulePath, { requires: {
|
||||
"settings-sharelatex": (this.Settings =
|
||||
{clsi: {docker: {}}}),
|
||||
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() })
|
||||
}
|
||||
});});
|
||||
|
||||
return describe("runWithLock", function() {
|
||||
describe("with a single lock", function() {
|
||||
beforeEach(function(done) {
|
||||
this.callback = sinon.stub();
|
||||
return this.LockManager.runWithLock("lock-one", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world")
|
||||
, 100)
|
||||
|
||||
, (err, ...args) => {
|
||||
this.callback(err,...Array.from(args));
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.calledWith(null,"hello","world").should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with two locks", function() {
|
||||
beforeEach(function(done) {
|
||||
this.callback1 = sinon.stub();
|
||||
this.callback2 = sinon.stub();
|
||||
this.LockManager.runWithLock("lock-one", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world","one")
|
||||
, 100)
|
||||
|
||||
, (err, ...args) => {
|
||||
return this.callback1(err,...Array.from(args));
|
||||
});
|
||||
return this.LockManager.runWithLock("lock-two", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world","two")
|
||||
, 200)
|
||||
|
||||
, (err, ...args) => {
|
||||
this.callback2(err,...Array.from(args));
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call the first callback", function() {
|
||||
return this.callback1.calledWith(null,"hello","world","one").should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the second callback", function() {
|
||||
return this.callback2.calledWith(null,"hello","world","two").should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with lock contention", function() {
|
||||
describe("where the first lock is released quickly", function() {
|
||||
beforeEach(function(done) {
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 1000;
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100;
|
||||
this.callback1 = sinon.stub();
|
||||
this.callback2 = sinon.stub();
|
||||
this.LockManager.runWithLock("lock", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world","one")
|
||||
, 100)
|
||||
|
||||
, (err, ...args) => {
|
||||
return this.callback1(err,...Array.from(args));
|
||||
});
|
||||
return this.LockManager.runWithLock("lock", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world","two")
|
||||
, 200)
|
||||
|
||||
, (err, ...args) => {
|
||||
this.callback2(err,...Array.from(args));
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call the first callback", function() {
|
||||
return this.callback1.calledWith(null,"hello","world","one").should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the second callback", function() {
|
||||
return this.callback2.calledWith(null,"hello","world","two").should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("where the first lock is held longer than the waiting time", function() {
|
||||
beforeEach(function(done) {
|
||||
let doneTwo;
|
||||
this.LockManager.MAX_LOCK_HOLD_TIME = 10000;
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 1000;
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100;
|
||||
this.callback1 = sinon.stub();
|
||||
this.callback2 = sinon.stub();
|
||||
let doneOne = (doneTwo = false);
|
||||
const finish = function(key) {
|
||||
if (key === 1) { doneOne = true; }
|
||||
if (key === 2) { doneTwo = true; }
|
||||
if (doneOne && doneTwo) { return done(); }
|
||||
};
|
||||
this.LockManager.runWithLock("lock", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world","one")
|
||||
, 1100)
|
||||
|
||||
, (err, ...args) => {
|
||||
this.callback1(err,...Array.from(args));
|
||||
return finish(1);
|
||||
});
|
||||
return this.LockManager.runWithLock("lock", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world","two")
|
||||
, 100)
|
||||
|
||||
, (err, ...args) => {
|
||||
this.callback2(err,...Array.from(args));
|
||||
return finish(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should call the first callback", function() {
|
||||
return this.callback1.calledWith(null,"hello","world","one").should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the second callback with an error", function() {
|
||||
const error = sinon.match.instanceOf(Error);
|
||||
return this.callback2.calledWith(error).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("where the first lock is held longer than the max holding time", function() {
|
||||
beforeEach(function(done) {
|
||||
let doneTwo;
|
||||
this.LockManager.MAX_LOCK_HOLD_TIME = 1000;
|
||||
this.LockManager.MAX_LOCK_WAIT_TIME = 2000;
|
||||
this.LockManager.LOCK_TEST_INTERVAL = 100;
|
||||
this.callback1 = sinon.stub();
|
||||
this.callback2 = sinon.stub();
|
||||
let doneOne = (doneTwo = false);
|
||||
const finish = function(key) {
|
||||
if (key === 1) { doneOne = true; }
|
||||
if (key === 2) { doneTwo = true; }
|
||||
if (doneOne && doneTwo) { return done(); }
|
||||
};
|
||||
this.LockManager.runWithLock("lock", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world","one")
|
||||
, 1500)
|
||||
|
||||
, (err, ...args) => {
|
||||
this.callback1(err,...Array.from(args));
|
||||
return finish(1);
|
||||
});
|
||||
return this.LockManager.runWithLock("lock", releaseLock =>
|
||||
setTimeout(() => releaseLock(null, "hello", "world","two")
|
||||
, 100)
|
||||
|
||||
, (err, ...args) => {
|
||||
this.callback2(err,...Array.from(args));
|
||||
return finish(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should call the first callback", function() {
|
||||
return this.callback1.calledWith(null,"hello","world","one").should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the second callback", function() {
|
||||
return this.callback2.calledWith(null,"hello","world","two").should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
663
test/unit/js/DockerRunnerTests.js
Normal file
663
test/unit/js/DockerRunnerTests.js
Normal file
@@ -0,0 +1,663 @@
|
||||
/* eslint-disable
|
||||
handle-callback-err,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const { expect } = require('chai');
|
||||
require("coffee-script");
|
||||
const modulePath = require('path').join(__dirname, '../../../app/coffee/DockerRunner');
|
||||
const Path = require("path");
|
||||
|
||||
describe("DockerRunner", function() {
|
||||
beforeEach(function() {
|
||||
let container, Docker, Timer;
|
||||
this.container = (container = {});
|
||||
this.DockerRunner = SandboxedModule.require(modulePath, { requires: {
|
||||
"settings-sharelatex": (this.Settings = {
|
||||
clsi: { docker: {}
|
||||
},
|
||||
path: {}
|
||||
}),
|
||||
"logger-sharelatex": (this.logger = {
|
||||
log: sinon.stub(),
|
||||
error: sinon.stub(),
|
||||
info: sinon.stub(),
|
||||
warn: sinon.stub()
|
||||
}),
|
||||
"dockerode": (Docker = (function() {
|
||||
Docker = class Docker {
|
||||
static initClass() {
|
||||
this.prototype.getContainer = sinon.stub().returns(container);
|
||||
this.prototype.createContainer = sinon.stub().yields(null, container);
|
||||
this.prototype.listContainers = sinon.stub();
|
||||
}
|
||||
};
|
||||
Docker.initClass();
|
||||
return Docker;
|
||||
})()),
|
||||
"fs": (this.fs = { stat: sinon.stub().yields(null,{isDirectory(){ return true; }}) }),
|
||||
"./Metrics": {
|
||||
Timer: (Timer = class Timer {
|
||||
done() {}
|
||||
})
|
||||
},
|
||||
"./LockManager": {
|
||||
runWithLock(key, runner, callback) { return runner(callback); }
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
this.Docker = Docker;
|
||||
this.getContainer = Docker.prototype.getContainer;
|
||||
this.createContainer = Docker.prototype.createContainer;
|
||||
this.listContainers = Docker.prototype.listContainers;
|
||||
|
||||
this.directory = "/local/compile/directory";
|
||||
this.mainFile = "main-file.tex";
|
||||
this.compiler = "pdflatex";
|
||||
this.image = "example.com/sharelatex/image:2016.2";
|
||||
this.env = {};
|
||||
this.callback = sinon.stub();
|
||||
this.project_id = "project-id-123";
|
||||
this.volumes =
|
||||
{"/local/compile/directory": "/compile"};
|
||||
this.Settings.clsi.docker.image = (this.defaultImage = "default-image");
|
||||
return this.Settings.clsi.docker.env = {PATH: "mock-path"};
|
||||
});
|
||||
|
||||
describe("run", function() {
|
||||
beforeEach(function(done){
|
||||
this.DockerRunner._getContainerOptions = sinon.stub().returns(this.options = {mockoptions: "foo"});
|
||||
this.DockerRunner._fingerprintContainer = sinon.stub().returns(this.fingerprint = "fingerprint");
|
||||
|
||||
this.name = `project-${this.project_id}-${this.fingerprint}`;
|
||||
|
||||
this.command = ["mock", "command", "--outdir=$COMPILE_DIR"];
|
||||
this.command_with_dir = ["mock", "command", "--outdir=/compile"];
|
||||
this.timeout = 42000;
|
||||
return done();
|
||||
});
|
||||
|
||||
describe("successfully", function() {
|
||||
beforeEach(function(done){
|
||||
this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output"));
|
||||
return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, (err, output)=> {
|
||||
this.callback(err, output);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should generate the options for the container", function() {
|
||||
return this.DockerRunner._getContainerOptions
|
||||
.calledWith(this.command_with_dir, this.image, this.volumes, this.timeout)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should generate the fingerprint from the returned options", function() {
|
||||
return this.DockerRunner._fingerprintContainer
|
||||
.calledWith(this.options)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should do the run", function() {
|
||||
return this.DockerRunner._runAndWaitForContainer
|
||||
.calledWith(this.options, this.volumes, this.timeout)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when path.sandboxedCompilesHostDir is set', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
this.Settings.path.sandboxedCompilesHostDir = '/some/host/dir/compiles';
|
||||
this.directory = '/var/lib/sharelatex/data/compiles/xyz';
|
||||
this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output"));
|
||||
return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback);
|
||||
});
|
||||
|
||||
it('should re-write the bind directory', function() {
|
||||
const volumes = this.DockerRunner._runAndWaitForContainer.lastCall.args[1];
|
||||
return expect(volumes).to.deep.equal({
|
||||
'/some/host/dir/compiles/xyz': '/compile'
|
||||
});
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the run throws an error", function() {
|
||||
beforeEach(function() {
|
||||
let firstTime = true;
|
||||
this.output = "mock-output";
|
||||
this.DockerRunner._runAndWaitForContainer = (options, volumes, timeout, callback) => {
|
||||
if (callback == null) { callback = function(error, output){}; }
|
||||
if (firstTime) {
|
||||
firstTime = false;
|
||||
return callback(new Error("HTTP code is 500 which indicates error: server error"));
|
||||
} else {
|
||||
return callback(null, this.output);
|
||||
}
|
||||
};
|
||||
sinon.spy(this.DockerRunner, "_runAndWaitForContainer");
|
||||
this.DockerRunner.destroyContainer = sinon.stub().callsArg(3);
|
||||
return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback);
|
||||
});
|
||||
|
||||
it("should do the run twice", function() {
|
||||
return this.DockerRunner._runAndWaitForContainer
|
||||
.calledTwice.should.equal(true);
|
||||
});
|
||||
|
||||
it("should destroy the container in between", function() {
|
||||
return this.DockerRunner.destroyContainer
|
||||
.calledWith(this.name, null)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with no image", function() {
|
||||
beforeEach(function() {
|
||||
this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output"));
|
||||
return this.DockerRunner.run(this.project_id, this.command, this.directory, null, this.timeout, this.env, this.callback);
|
||||
});
|
||||
|
||||
return it("should use the default image", function() {
|
||||
return this.DockerRunner._getContainerOptions
|
||||
.calledWith(this.command_with_dir, this.defaultImage, this.volumes, this.timeout)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with image override", function() {
|
||||
beforeEach(function() {
|
||||
this.Settings.texliveImageNameOveride = "overrideimage.com/something";
|
||||
this.DockerRunner._runAndWaitForContainer = sinon.stub().callsArgWith(3, null, (this.output = "mock-output"));
|
||||
return this.DockerRunner.run(this.project_id, this.command, this.directory, this.image, this.timeout, this.env, this.callback);
|
||||
});
|
||||
|
||||
return it("should use the override and keep the tag", function() {
|
||||
const image = this.DockerRunner._getContainerOptions.args[0][1];
|
||||
return image.should.equal("overrideimage.com/something/image:2016.2");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("_runAndWaitForContainer", function() {
|
||||
beforeEach(function() {
|
||||
this.options = {mockoptions: "foo", name: (this.name = "mock-name")};
|
||||
this.DockerRunner.startContainer = (options, volumes, attachStreamHandler, callback) => {
|
||||
attachStreamHandler(null, (this.output = "mock-output"));
|
||||
return callback(null, (this.containerId = "container-id"));
|
||||
};
|
||||
sinon.spy(this.DockerRunner, "startContainer");
|
||||
this.DockerRunner.waitForContainer = sinon.stub().callsArgWith(2, null, (this.exitCode = 42));
|
||||
return this.DockerRunner._runAndWaitForContainer(this.options, this.volumes, this.timeout, this.callback);
|
||||
});
|
||||
|
||||
it("should create/start the container", function() {
|
||||
return this.DockerRunner.startContainer
|
||||
.calledWith(this.options, this.volumes)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should wait for the container to finish", function() {
|
||||
return this.DockerRunner.waitForContainer
|
||||
.calledWith(this.name, this.timeout)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with the output", function() {
|
||||
return this.callback.calledWith(null, this.output).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("startContainer", function() {
|
||||
beforeEach(function() {
|
||||
this.attachStreamHandler = sinon.stub();
|
||||
this.attachStreamHandler.cock = true;
|
||||
this.options = {mockoptions: "foo", name: "mock-name"};
|
||||
this.container.inspect = sinon.stub().callsArgWith(0);
|
||||
this.DockerRunner.attachToContainer = (containerId, attachStreamHandler, cb)=> {
|
||||
attachStreamHandler();
|
||||
return cb();
|
||||
};
|
||||
return sinon.spy(this.DockerRunner, "attachToContainer");
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe("when the container exists", function() {
|
||||
beforeEach(function() {
|
||||
this.container.inspect = sinon.stub().callsArgWith(0);
|
||||
this.container.start = sinon.stub().yields();
|
||||
|
||||
return this.DockerRunner.startContainer(this.options, this.volumes, this.callback, () => {});
|
||||
});
|
||||
|
||||
it("should start the container with the given name", function() {
|
||||
this.getContainer
|
||||
.calledWith(this.options.name)
|
||||
.should.equal(true);
|
||||
return this.container.start
|
||||
.called
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should not try to create the container", function() {
|
||||
return this.createContainer.called.should.equal(false);
|
||||
});
|
||||
|
||||
it("should attach to the container", function() {
|
||||
return this.DockerRunner.attachToContainer.called.should.equal(true);
|
||||
});
|
||||
|
||||
it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should attach before the container starts", function() {
|
||||
return sinon.assert.callOrder(this.DockerRunner.attachToContainer, this.container.start);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the container does not exist", function() {
|
||||
beforeEach(function(){
|
||||
const exists = false;
|
||||
this.container.start = sinon.stub().yields();
|
||||
this.container.inspect = sinon.stub().callsArgWith(0, {statusCode:404});
|
||||
return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback);
|
||||
});
|
||||
|
||||
it("should create the container", function() {
|
||||
return this.createContainer
|
||||
.calledWith(this.options)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should call the callback and stream handler", function() {
|
||||
this.attachStreamHandler.called.should.equal(true);
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
|
||||
it("should attach to the container", function() {
|
||||
return this.DockerRunner.attachToContainer.called.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should attach before the container starts", function() {
|
||||
return sinon.assert.callOrder(this.DockerRunner.attachToContainer, this.container.start);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("when the container is already running", function() {
|
||||
beforeEach(function() {
|
||||
const error = new Error(`HTTP code is 304 which indicates error: server error - start: Cannot start container ${this.name}: The container MOCKID is already running.`);
|
||||
error.statusCode = 304;
|
||||
this.container.start = sinon.stub().yields(error);
|
||||
this.container.inspect = sinon.stub().callsArgWith(0);
|
||||
return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback);
|
||||
});
|
||||
|
||||
it("should not try to create the container", function() {
|
||||
return this.createContainer.called.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should call the callback and stream handler without an error", function() {
|
||||
this.attachStreamHandler.called.should.equal(true);
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a volume does not exist", function() {
|
||||
beforeEach(function(){
|
||||
this.fs.stat = sinon.stub().yields(new Error("no such path"));
|
||||
return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback);
|
||||
});
|
||||
|
||||
it("should not try to create the container", function() {
|
||||
return this.createContainer.called.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should call the callback with an error", function() {
|
||||
return this.callback.calledWith(new Error()).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a volume exists but is not a directory", function() {
|
||||
beforeEach(function() {
|
||||
this.fs.stat = sinon.stub().yields(null, {isDirectory() { return false; }});
|
||||
return this.DockerRunner.startContainer(this.options, this.volumes, this.attachStreamHandler, this.callback);
|
||||
});
|
||||
|
||||
it("should not try to create the container", function() {
|
||||
return this.createContainer.called.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should call the callback with an error", function() {
|
||||
return this.callback.calledWith(new Error()).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a volume does not exist, but sibling-containers are used", function() {
|
||||
beforeEach(function() {
|
||||
this.fs.stat = sinon.stub().yields(new Error("no such path"));
|
||||
this.Settings.path.sandboxedCompilesHostDir = '/some/path';
|
||||
this.container.start = sinon.stub().yields();
|
||||
return this.DockerRunner.startContainer(this.options, this.volumes, this.callback);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
return delete this.Settings.path.sandboxedCompilesHostDir;
|
||||
});
|
||||
|
||||
it("should start the container with the given name", function() {
|
||||
this.getContainer
|
||||
.calledWith(this.options.name)
|
||||
.should.equal(true);
|
||||
return this.container.start
|
||||
.called
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should not try to create the container", function() {
|
||||
return this.createContainer.called.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
this.callback.called.should.equal(true);
|
||||
return this.callback.calledWith(new Error()).should.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the container tries to be created, but already has been (race condition)", function() {});
|
||||
});
|
||||
|
||||
describe("waitForContainer", function() {
|
||||
beforeEach(function() {
|
||||
this.containerId = "container-id";
|
||||
this.timeout = 5000;
|
||||
this.container.wait = sinon.stub().yields(null, {StatusCode: (this.statusCode = 42)});
|
||||
return this.container.kill = sinon.stub().yields();
|
||||
});
|
||||
|
||||
describe("when the container returns in time", function() {
|
||||
beforeEach(function() {
|
||||
return this.DockerRunner.waitForContainer(this.containerId, this.timeout, this.callback);
|
||||
});
|
||||
|
||||
it("should wait for the container", function() {
|
||||
this.getContainer
|
||||
.calledWith(this.containerId)
|
||||
.should.equal(true);
|
||||
return this.container.wait
|
||||
.called
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with the exit", function() {
|
||||
return this.callback
|
||||
.calledWith(null, this.statusCode)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the container does not return before the timeout", function() {
|
||||
beforeEach(function(done) {
|
||||
this.container.wait = function(callback) {
|
||||
if (callback == null) { callback = function(error, exitCode) {}; }
|
||||
return setTimeout(() => callback(null, {StatusCode: 42})
|
||||
, 100);
|
||||
};
|
||||
this.timeout = 5;
|
||||
return this.DockerRunner.waitForContainer(this.containerId, this.timeout, (...args) => {
|
||||
this.callback(...Array.from(args || []));
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call kill on the container", function() {
|
||||
this.getContainer
|
||||
.calledWith(this.containerId)
|
||||
.should.equal(true);
|
||||
return this.container.kill
|
||||
.called
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with an error", function() {
|
||||
const error = new Error("container timed out");
|
||||
error.timedout = true;
|
||||
return this.callback
|
||||
.calledWith(error)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("destroyOldContainers", function() {
|
||||
beforeEach(function(done) {
|
||||
const oneHourInSeconds = 60 * 60;
|
||||
const oneHourInMilliseconds = oneHourInSeconds * 1000;
|
||||
const nowInSeconds = Date.now()/1000;
|
||||
this.containers = [{
|
||||
Name: "/project-old-container-name",
|
||||
Id: "old-container-id",
|
||||
Created: nowInSeconds - oneHourInSeconds - 100
|
||||
}, {
|
||||
Name: "/project-new-container-name",
|
||||
Id: "new-container-id",
|
||||
Created: (nowInSeconds - oneHourInSeconds) + 100
|
||||
}, {
|
||||
Name: "/totally-not-a-project-container",
|
||||
Id: "some-random-id",
|
||||
Created: nowInSeconds - (2 * oneHourInSeconds )
|
||||
}];
|
||||
this.DockerRunner.MAX_CONTAINER_AGE = oneHourInMilliseconds;
|
||||
this.listContainers.callsArgWith(1, null, this.containers);
|
||||
this.DockerRunner.destroyContainer = sinon.stub().callsArg(3);
|
||||
return this.DockerRunner.destroyOldContainers(error => {
|
||||
this.callback(error);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should list all containers", function() {
|
||||
return this.listContainers
|
||||
.calledWith({all: true})
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should destroy old containers", function() {
|
||||
this.DockerRunner.destroyContainer
|
||||
.callCount
|
||||
.should.equal(1);
|
||||
return this.DockerRunner.destroyContainer
|
||||
.calledWith("/project-old-container-name", "old-container-id")
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should not destroy new containers", function() {
|
||||
return this.DockerRunner.destroyContainer
|
||||
.calledWith("/project-new-container-name", "new-container-id")
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not destroy non-project containers", function() {
|
||||
return this.DockerRunner.destroyContainer
|
||||
.calledWith("/totally-not-a-project-container", "some-random-id")
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should callback the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('_destroyContainer', function() {
|
||||
beforeEach(function() {
|
||||
this.containerId = 'some_id';
|
||||
this.fakeContainer =
|
||||
{remove: sinon.stub().callsArgWith(1, null)};
|
||||
return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
|
||||
});
|
||||
|
||||
it('should get the container', function(done) {
|
||||
return this.DockerRunner._destroyContainer(this.containerId, false, err => {
|
||||
this.Docker.prototype.getContainer.callCount.should.equal(1);
|
||||
this.Docker.prototype.getContainer.calledWith(this.containerId).should.equal(true);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should try to force-destroy the container when shouldForce=true', function(done) {
|
||||
return this.DockerRunner._destroyContainer(this.containerId, true, err => {
|
||||
this.fakeContainer.remove.callCount.should.equal(1);
|
||||
this.fakeContainer.remove.calledWith({force: true}).should.equal(true);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not try to force-destroy the container when shouldForce=false', function(done) {
|
||||
return this.DockerRunner._destroyContainer(this.containerId, false, err => {
|
||||
this.fakeContainer.remove.callCount.should.equal(1);
|
||||
this.fakeContainer.remove.calledWith({force: false}).should.equal(true);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not produce an error', function(done) {
|
||||
return this.DockerRunner._destroyContainer(this.containerId, false, err => {
|
||||
expect(err).to.equal(null);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the container is already gone', function() {
|
||||
beforeEach(function() {
|
||||
this.fakeError = new Error('woops');
|
||||
this.fakeError.statusCode = 404;
|
||||
this.fakeContainer =
|
||||
{remove: sinon.stub().callsArgWith(1, this.fakeError)};
|
||||
return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
|
||||
});
|
||||
|
||||
return it('should not produce an error', function(done) {
|
||||
return this.DockerRunner._destroyContainer(this.containerId, false, err => {
|
||||
expect(err).to.equal(null);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return describe('when container.destroy produces an error', function(done) {
|
||||
beforeEach(function() {
|
||||
this.fakeError = new Error('woops');
|
||||
this.fakeError.statusCode = 500;
|
||||
this.fakeContainer =
|
||||
{remove: sinon.stub().callsArgWith(1, this.fakeError)};
|
||||
return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
|
||||
});
|
||||
|
||||
return it('should produce an error', function(done) {
|
||||
return this.DockerRunner._destroyContainer(this.containerId, false, err => {
|
||||
expect(err).to.not.equal(null);
|
||||
expect(err).to.equal(this.fakeError);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return describe('kill', function() {
|
||||
beforeEach(function() {
|
||||
this.containerId = 'some_id';
|
||||
this.fakeContainer =
|
||||
{kill: sinon.stub().callsArgWith(0, null)};
|
||||
return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
|
||||
});
|
||||
|
||||
it('should get the container', function(done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
this.Docker.prototype.getContainer.callCount.should.equal(1);
|
||||
this.Docker.prototype.getContainer.calledWith(this.containerId).should.equal(true);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should try to force-destroy the container', function(done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
this.fakeContainer.kill.callCount.should.equal(1);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not produce an error', function(done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
expect(err).to.equal(undefined);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the container is not actually running', function() {
|
||||
beforeEach(function() {
|
||||
this.fakeError = new Error('woops');
|
||||
this.fakeError.statusCode = 500;
|
||||
this.fakeError.message = 'Cannot kill container <whatever> is not running';
|
||||
this.fakeContainer =
|
||||
{kill: sinon.stub().callsArgWith(0, this.fakeError)};
|
||||
return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
|
||||
});
|
||||
|
||||
return it('should not produce an error', function(done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
expect(err).to.equal(undefined);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return describe('when container.kill produces a legitimate error', function(done) {
|
||||
beforeEach(function() {
|
||||
this.fakeError = new Error('woops');
|
||||
this.fakeError.statusCode = 500;
|
||||
this.fakeError.message = 'Totally legitimate reason to throw an error';
|
||||
this.fakeContainer =
|
||||
{kill: sinon.stub().callsArgWith(0, this.fakeError)};
|
||||
return this.Docker.prototype.getContainer = sinon.stub().returns(this.fakeContainer);
|
||||
});
|
||||
|
||||
return it('should produce an error', function(done) {
|
||||
return this.DockerRunner.kill(this.containerId, err => {
|
||||
expect(err).to.not.equal(undefined);
|
||||
expect(err).to.equal(this.fakeError);
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
82
test/unit/js/DraftModeManagerTests.js
Normal file
82
test/unit/js/DraftModeManagerTests.js
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/DraftModeManager');
|
||||
|
||||
describe('DraftModeManager', function() {
|
||||
beforeEach(function() {
|
||||
return this.DraftModeManager = SandboxedModule.require(modulePath, { requires: {
|
||||
"fs": (this.fs = {}),
|
||||
"logger-sharelatex": (this.logger = {log() {}})
|
||||
}
|
||||
});});
|
||||
|
||||
describe("_injectDraftOption", function() {
|
||||
it("should add draft option into documentclass with existing options", function() {
|
||||
return this.DraftModeManager
|
||||
._injectDraftOption(`\
|
||||
\\documentclass[a4paper,foo=bar]{article}\
|
||||
`)
|
||||
.should.equal(`\
|
||||
\\documentclass[draft,a4paper,foo=bar]{article}\
|
||||
`);
|
||||
});
|
||||
|
||||
return it("should add draft option into documentclass with no options", function() {
|
||||
return this.DraftModeManager
|
||||
._injectDraftOption(`\
|
||||
\\documentclass{article}\
|
||||
`)
|
||||
.should.equal(`\
|
||||
\\documentclass[draft]{article}\
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("injectDraftMode", function() {
|
||||
beforeEach(function() {
|
||||
this.filename = "/mock/filename.tex";
|
||||
this.callback = sinon.stub();
|
||||
const content = `\
|
||||
\\documentclass{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`;
|
||||
this.fs.readFile = sinon.stub().callsArgWith(2, null, content);
|
||||
this.fs.writeFile = sinon.stub().callsArg(2);
|
||||
return this.DraftModeManager.injectDraftMode(this.filename, this.callback);
|
||||
});
|
||||
|
||||
it("should read the file", function() {
|
||||
return this.fs.readFile
|
||||
.calledWith(this.filename, "utf8")
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should write the modified file", function() {
|
||||
return this.fs.writeFile
|
||||
.calledWith(this.filename, `\
|
||||
\\documentclass[draft]{article}
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
111
test/unit/js/LatexRunnerTests.js
Normal file
111
test/unit/js/LatexRunnerTests.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/LatexRunner');
|
||||
const Path = require("path");
|
||||
|
||||
describe("LatexRunner", function() {
|
||||
beforeEach(function() {
|
||||
let Timer;
|
||||
this.LatexRunner = SandboxedModule.require(modulePath, { requires: {
|
||||
"settings-sharelatex": (this.Settings = {
|
||||
docker: {
|
||||
socketPath: "/var/run/docker.sock"
|
||||
}
|
||||
}),
|
||||
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
|
||||
"./Metrics": {
|
||||
Timer: (Timer = class Timer {
|
||||
done() {}
|
||||
})
|
||||
},
|
||||
"./CommandRunner": (this.CommandRunner = {})
|
||||
}
|
||||
});
|
||||
|
||||
this.directory = "/local/compile/directory";
|
||||
this.mainFile = "main-file.tex";
|
||||
this.compiler = "pdflatex";
|
||||
this.image = "example.com/image";
|
||||
this.callback = sinon.stub();
|
||||
this.project_id = "project-id-123";
|
||||
return this.env = {'foo': '123'};});
|
||||
|
||||
return describe("runLatex", function() {
|
||||
beforeEach(function() {
|
||||
return this.CommandRunner.run = sinon.stub().callsArg(6);
|
||||
});
|
||||
|
||||
describe("normally", function() {
|
||||
beforeEach(function() {
|
||||
return this.LatexRunner.runLatex(this.project_id, {
|
||||
directory: this.directory,
|
||||
mainFile: this.mainFile,
|
||||
compiler: this.compiler,
|
||||
timeout: (this.timeout = 42000),
|
||||
image: this.image,
|
||||
environment: this.env
|
||||
},
|
||||
this.callback);
|
||||
});
|
||||
|
||||
return it("should run the latex command", function() {
|
||||
return this.CommandRunner.run
|
||||
.calledWith(this.project_id, sinon.match.any, this.directory, this.image, this.timeout, this.env)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with an .Rtex main file", function() {
|
||||
beforeEach(function() {
|
||||
return this.LatexRunner.runLatex(this.project_id, {
|
||||
directory: this.directory,
|
||||
mainFile: "main-file.Rtex",
|
||||
compiler: this.compiler,
|
||||
image: this.image,
|
||||
timeout: (this.timeout = 42000)
|
||||
},
|
||||
this.callback);
|
||||
});
|
||||
|
||||
return it("should run the latex command on the equivalent .tex file", function() {
|
||||
const command = this.CommandRunner.run.args[0][1];
|
||||
const mainFile = command.slice(-1)[0];
|
||||
return mainFile.should.equal("$COMPILE_DIR/main-file.tex");
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with a flags option", function() {
|
||||
beforeEach(function() {
|
||||
return this.LatexRunner.runLatex(this.project_id, {
|
||||
directory: this.directory,
|
||||
mainFile: this.mainFile,
|
||||
compiler: this.compiler,
|
||||
image: this.image,
|
||||
timeout: (this.timeout = 42000),
|
||||
flags: ["-file-line-error", "-halt-on-error"]
|
||||
},
|
||||
this.callback);
|
||||
});
|
||||
|
||||
return it("should include the flags in the command", function() {
|
||||
const command = this.CommandRunner.run.args[0][1];
|
||||
const flags = command.filter(arg => (arg === "-file-line-error") || (arg === "-halt-on-error"));
|
||||
flags.length.should.equal(2);
|
||||
flags[0].should.equal("-file-line-error");
|
||||
return flags[1].should.equal("-halt-on-error");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
83
test/unit/js/LockManagerTests.js
Normal file
83
test/unit/js/LockManagerTests.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/LockManager');
|
||||
const Path = require("path");
|
||||
const Errors = require("../../../app/js/Errors");
|
||||
|
||||
describe("DockerLockManager", function() {
|
||||
beforeEach(function() {
|
||||
this.LockManager = SandboxedModule.require(modulePath, { requires: {
|
||||
"settings-sharelatex": {},
|
||||
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), err() {} }),
|
||||
"fs": {
|
||||
lstat:sinon.stub().callsArgWith(1),
|
||||
readdir: sinon.stub().callsArgWith(1)
|
||||
},
|
||||
"lockfile": (this.Lockfile = {})
|
||||
}
|
||||
});
|
||||
return this.lockFile = "/local/compile/directory/.project-lock";
|
||||
});
|
||||
|
||||
return describe("runWithLock", function() {
|
||||
beforeEach(function() {
|
||||
this.runner = sinon.stub().callsArgWith(0, null, "foo", "bar");
|
||||
return this.callback = sinon.stub();
|
||||
});
|
||||
|
||||
describe("normally", function() {
|
||||
beforeEach(function() {
|
||||
this.Lockfile.lock = sinon.stub().callsArgWith(2, null);
|
||||
this.Lockfile.unlock = sinon.stub().callsArgWith(1, null);
|
||||
return this.LockManager.runWithLock(this.lockFile, this.runner, this.callback);
|
||||
});
|
||||
|
||||
it("should run the compile", function() {
|
||||
return this.runner
|
||||
.calledWith()
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with the response from the compile", function() {
|
||||
return this.callback
|
||||
.calledWithExactly(null, "foo", "bar")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the project is locked", function() {
|
||||
beforeEach(function() {
|
||||
this.error = new Error();
|
||||
this.error.code = "EEXIST";
|
||||
this.Lockfile.lock = sinon.stub().callsArgWith(2,this.error);
|
||||
this.Lockfile.unlock = sinon.stub().callsArgWith(1, null);
|
||||
return this.LockManager.runWithLock(this.lockFile, this.runner, this.callback);
|
||||
});
|
||||
|
||||
it("should not run the compile", function() {
|
||||
return this.runner
|
||||
.called
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
const error = new Errors.AlreadyCompilingError();
|
||||
return this.callback
|
||||
.calledWithExactly(error)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
99
test/unit/js/OutputFileFinderTests.js
Normal file
99
test/unit/js/OutputFileFinderTests.js
Normal file
@@ -0,0 +1,99 @@
|
||||
/* eslint-disable
|
||||
handle-callback-err,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/OutputFileFinder');
|
||||
const path = require("path");
|
||||
const { expect } = require("chai");
|
||||
const { EventEmitter } = require("events");
|
||||
|
||||
describe("OutputFileFinder", function() {
|
||||
beforeEach(function() {
|
||||
this.OutputFileFinder = SandboxedModule.require(modulePath, { requires: {
|
||||
"fs": (this.fs = {}),
|
||||
"child_process": { spawn: (this.spawn = sinon.stub())
|
||||
},
|
||||
"logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() }
|
||||
}
|
||||
});
|
||||
this.directory = "/test/dir";
|
||||
return this.callback = sinon.stub();
|
||||
});
|
||||
|
||||
describe("findOutputFiles", function() {
|
||||
beforeEach(function() {
|
||||
this.resource_path = "resource/path.tex";
|
||||
this.output_paths = ["output.pdf", "extra/file.tex"];
|
||||
this.all_paths = this.output_paths.concat([this.resource_path]);
|
||||
this.resources = [
|
||||
{path: (this.resource_path = "resource/path.tex")}
|
||||
];
|
||||
this.OutputFileFinder._getAllFiles = sinon.stub().callsArgWith(1, null, this.all_paths);
|
||||
return this.OutputFileFinder.findOutputFiles(this.resources, this.directory, (error, outputFiles) => {
|
||||
this.outputFiles = outputFiles;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return it("should only return the output files, not directories or resource paths", function() {
|
||||
return expect(this.outputFiles).to.deep.equal([{
|
||||
path: "output.pdf",
|
||||
type: "pdf"
|
||||
}, {
|
||||
path: "extra/file.tex",
|
||||
type: "tex"
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("_getAllFiles", function() {
|
||||
beforeEach(function() {
|
||||
this.proc = new EventEmitter();
|
||||
this.proc.stdout = new EventEmitter();
|
||||
this.spawn.returns(this.proc);
|
||||
this.directory = "/base/dir";
|
||||
return this.OutputFileFinder._getAllFiles(this.directory, this.callback);
|
||||
});
|
||||
|
||||
describe("successfully", function() {
|
||||
beforeEach(function() {
|
||||
this.proc.stdout.emit(
|
||||
"data",
|
||||
["/base/dir/main.tex", "/base/dir/chapters/chapter1.tex"].join("\n") + "\n"
|
||||
);
|
||||
return this.proc.emit("close", 0);
|
||||
});
|
||||
|
||||
return it("should call the callback with the relative file paths", function() {
|
||||
return this.callback.calledWith(
|
||||
null,
|
||||
["main.tex", "chapters/chapter1.tex"]
|
||||
).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the directory doesn't exist", function() {
|
||||
beforeEach(function() {
|
||||
return this.proc.emit("close", 1);
|
||||
});
|
||||
|
||||
return it("should call the callback with a blank array", function() {
|
||||
return this.callback.calledWith(
|
||||
null,
|
||||
[]
|
||||
).should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
148
test/unit/js/OutputFileOptimiserTests.js
Normal file
148
test/unit/js/OutputFileOptimiserTests.js
Normal file
@@ -0,0 +1,148 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
node/no-deprecated-api,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/OutputFileOptimiser');
|
||||
const path = require("path");
|
||||
const { expect } = require("chai");
|
||||
const { EventEmitter } = require("events");
|
||||
|
||||
describe("OutputFileOptimiser", function() {
|
||||
beforeEach(function() {
|
||||
this.OutputFileOptimiser = SandboxedModule.require(modulePath, { requires: {
|
||||
"fs": (this.fs = {}),
|
||||
"path": (this.Path = {}),
|
||||
"child_process": { spawn: (this.spawn = sinon.stub())
|
||||
},
|
||||
"logger-sharelatex": { log: sinon.stub(), warn: sinon.stub() },
|
||||
"./Metrics" : {}
|
||||
}
|
||||
});
|
||||
this.directory = "/test/dir";
|
||||
return this.callback = sinon.stub();
|
||||
});
|
||||
|
||||
describe("optimiseFile", function() {
|
||||
beforeEach(function() {
|
||||
this.src = "./output.pdf";
|
||||
return this.dst = "./output.pdf";
|
||||
});
|
||||
|
||||
describe("when the file is not a pdf file", function() {
|
||||
beforeEach(function(done){
|
||||
this.src = "./output.log";
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false);
|
||||
this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null);
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done);
|
||||
});
|
||||
|
||||
it("should not check if the file is optimised", function() {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(false);
|
||||
});
|
||||
|
||||
return it("should not optimise the file", function() {
|
||||
return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the pdf file is not optimised", function() {
|
||||
beforeEach(function(done) {
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, false);
|
||||
this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null);
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done);
|
||||
});
|
||||
|
||||
it("should check if the pdf is optimised", function() {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(true);
|
||||
});
|
||||
|
||||
return it("should optimise the pdf", function() {
|
||||
return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the pdf file is optimised", function() {
|
||||
beforeEach(function(done) {
|
||||
this.OutputFileOptimiser.checkIfPDFIsOptimised = sinon.stub().callsArgWith(1, null, true);
|
||||
this.OutputFileOptimiser.optimisePDF = sinon.stub().callsArgWith(2, null);
|
||||
return this.OutputFileOptimiser.optimiseFile(this.src, this.dst, done);
|
||||
});
|
||||
|
||||
it("should check if the pdf is optimised", function() {
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised.calledWith(this.src).should.equal(true);
|
||||
});
|
||||
|
||||
return it("should not optimise the pdf", function() {
|
||||
return this.OutputFileOptimiser.optimisePDF.calledWith(this.src, this.dst).should.equal(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return describe("checkIfPDFISOptimised", function() {
|
||||
beforeEach(function() {
|
||||
this.callback = sinon.stub();
|
||||
this.fd = 1234;
|
||||
this.fs.open = sinon.stub().yields(null, this.fd);
|
||||
this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello /Linearized 1"));
|
||||
this.fs.close = sinon.stub().withArgs(this.fd).yields(null);
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback);
|
||||
});
|
||||
|
||||
describe("for a linearised file", function() {
|
||||
beforeEach(function() {
|
||||
this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello /Linearized 1"));
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback);
|
||||
});
|
||||
|
||||
it("should open the file", function() {
|
||||
return this.fs.open.calledWith(this.src, "r").should.equal(true);
|
||||
});
|
||||
|
||||
it("should read the header", function() {
|
||||
return this.fs.read.calledWith(this.fd).should.equal(true);
|
||||
});
|
||||
|
||||
it("should close the file", function() {
|
||||
return this.fs.close.calledWith(this.fd).should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with a true result", function() {
|
||||
return this.callback.calledWith(null, true).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("for an unlinearised file", function() {
|
||||
beforeEach(function() {
|
||||
this.fs.read = sinon.stub().withArgs(this.fd).yields(null, 100, new Buffer("hello not linearized 1"));
|
||||
return this.OutputFileOptimiser.checkIfPDFIsOptimised(this.src, this.callback);
|
||||
});
|
||||
|
||||
it("should open the file", function() {
|
||||
return this.fs.open.calledWith(this.src, "r").should.equal(true);
|
||||
});
|
||||
|
||||
it("should read the header", function() {
|
||||
return this.fs.read.calledWith(this.fd).should.equal(true);
|
||||
});
|
||||
|
||||
it("should close the file", function() {
|
||||
return this.fs.close.calledWith(this.fd).should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with a false result", function() {
|
||||
return this.callback.calledWith(null, false).should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
89
test/unit/js/ProjectPersistenceManagerTests.js
Normal file
89
test/unit/js/ProjectPersistenceManagerTests.js
Normal file
@@ -0,0 +1,89 @@
|
||||
/* eslint-disable
|
||||
camelcase,
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/ProjectPersistenceManager');
|
||||
const tk = require("timekeeper");
|
||||
|
||||
describe("ProjectPersistenceManager", function() {
|
||||
beforeEach(function() {
|
||||
this.ProjectPersistenceManager = SandboxedModule.require(modulePath, { requires: {
|
||||
"./UrlCache": (this.UrlCache = {}),
|
||||
"./CompileManager": (this.CompileManager = {}),
|
||||
"logger-sharelatex": (this.logger = { log: sinon.stub() }),
|
||||
"./db": (this.db = {})
|
||||
}
|
||||
});
|
||||
this.callback = sinon.stub();
|
||||
this.project_id = "project-id-123";
|
||||
return this.user_id = "1234";
|
||||
});
|
||||
|
||||
describe("clearExpiredProjects", function() {
|
||||
beforeEach(function() {
|
||||
this.project_ids = [
|
||||
"project-id-1",
|
||||
"project-id-2"
|
||||
];
|
||||
this.ProjectPersistenceManager._findExpiredProjectIds = sinon.stub().callsArgWith(0, null, this.project_ids);
|
||||
this.ProjectPersistenceManager.clearProjectFromCache = sinon.stub().callsArg(1);
|
||||
this.CompileManager.clearExpiredProjects = sinon.stub().callsArg(1);
|
||||
return this.ProjectPersistenceManager.clearExpiredProjects(this.callback);
|
||||
});
|
||||
|
||||
it("should clear each expired project", function() {
|
||||
return Array.from(this.project_ids).map((project_id) =>
|
||||
this.ProjectPersistenceManager.clearProjectFromCache
|
||||
.calledWith(project_id)
|
||||
.should.equal(true));
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("clearProject", function() {
|
||||
beforeEach(function() {
|
||||
this.ProjectPersistenceManager._clearProjectFromDatabase = sinon.stub().callsArg(1);
|
||||
this.UrlCache.clearProject = sinon.stub().callsArg(1);
|
||||
this.CompileManager.clearProject = sinon.stub().callsArg(2);
|
||||
return this.ProjectPersistenceManager.clearProject(this.project_id, this.user_id, this.callback);
|
||||
});
|
||||
|
||||
it("should clear the project from the database", function() {
|
||||
return this.ProjectPersistenceManager._clearProjectFromDatabase
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should clear all the cached Urls for the project", function() {
|
||||
return this.UrlCache.clearProject
|
||||
.calledWith(this.project_id)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should clear the project compile folder", function() {
|
||||
return this.CompileManager.clearProject
|
||||
.calledWith(this.project_id, this.user_id)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
386
test/unit/js/RequestParserTests.js
Normal file
386
test/unit/js/RequestParserTests.js
Normal file
@@ -0,0 +1,386 @@
|
||||
/* eslint-disable
|
||||
handle-callback-err,
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const { expect } = require('chai');
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/RequestParser');
|
||||
const tk = require("timekeeper");
|
||||
|
||||
describe("RequestParser", function() {
|
||||
beforeEach(function() {
|
||||
tk.freeze();
|
||||
this.callback = sinon.stub();
|
||||
this.validResource = {
|
||||
path: "main.tex",
|
||||
date: "12:00 01/02/03",
|
||||
content: "Hello world"
|
||||
};
|
||||
this.validRequest = {
|
||||
compile: {
|
||||
token: "token-123",
|
||||
options: {
|
||||
imageName: "basicImageName/here:2017-1",
|
||||
compiler: "pdflatex",
|
||||
timeout: 42
|
||||
},
|
||||
resources: []
|
||||
}
|
||||
};
|
||||
return this.RequestParser = SandboxedModule.require(modulePath, { requires: {
|
||||
"settings-sharelatex": (this.settings = {})
|
||||
}
|
||||
});});
|
||||
|
||||
afterEach(function() { return tk.reset(); });
|
||||
|
||||
describe("without a top level object", function() {
|
||||
beforeEach(function() {
|
||||
return this.RequestParser.parse([], this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("top level object should have a compile attribute")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without a compile attribute", function() {
|
||||
beforeEach(function() {
|
||||
return this.RequestParser.parse({}, this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("top level object should have a compile attribute")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without a valid compiler", function() {
|
||||
beforeEach(function() {
|
||||
this.validRequest.compile.options.compiler = "not-a-compiler";
|
||||
return this.RequestParser.parse(this.validRequest, this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("compiler attribute should be one of: pdflatex, latex, xelatex, lualatex")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without a compiler specified", function() {
|
||||
beforeEach(function() {
|
||||
delete this.validRequest.compile.options.compiler;
|
||||
return this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
this.data = data;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return it("should set the compiler to pdflatex by default", function() {
|
||||
return this.data.compiler.should.equal("pdflatex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with imageName set", function() {
|
||||
beforeEach(function() {
|
||||
return this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
this.data = data;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return it("should set the imageName", function() {
|
||||
return this.data.imageName.should.equal("basicImageName/here:2017-1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with flags set", function() {
|
||||
beforeEach(function() {
|
||||
this.validRequest.compile.options.flags = ["-file-line-error"];
|
||||
return this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
this.data = data;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return it("should set the flags attribute", function() {
|
||||
return expect(this.data.flags).to.deep.equal(["-file-line-error"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with flags not specified", function() {
|
||||
beforeEach(function() {
|
||||
return this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
this.data = data;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return it("it should have an empty flags list", function() {
|
||||
return expect(this.data.flags).to.deep.equal([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without a timeout specified", function() {
|
||||
beforeEach(function() {
|
||||
delete this.validRequest.compile.options.timeout;
|
||||
return this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
this.data = data;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return it("should set the timeout to MAX_TIMEOUT", function() {
|
||||
return this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a timeout larger than the maximum", function() {
|
||||
beforeEach(function() {
|
||||
this.validRequest.compile.options.timeout = this.RequestParser.MAX_TIMEOUT + 1;
|
||||
return this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
this.data = data;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return it("should set the timeout to MAX_TIMEOUT", function() {
|
||||
return this.data.timeout.should.equal(this.RequestParser.MAX_TIMEOUT * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a timeout", function() {
|
||||
beforeEach(function() {
|
||||
return this.RequestParser.parse(this.validRequest, (error, data) => {
|
||||
this.data = data;
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
return it("should set the timeout (in milliseconds)", function() {
|
||||
return this.data.timeout.should.equal(this.validRequest.compile.options.timeout * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource without a path", function() {
|
||||
beforeEach(function() {
|
||||
delete this.validResource.path;
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
return this.RequestParser.parse(this.validRequest, this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("all resources should have a path attribute")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource with a path", function() {
|
||||
beforeEach(function() {
|
||||
this.validResource.path = (this.path = "test.tex");
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
this.RequestParser.parse(this.validRequest, this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should return the path in the parsed response", function() {
|
||||
return this.data.resources[0].path.should.equal(this.path);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource with a malformed modified date", function() {
|
||||
beforeEach(function() {
|
||||
this.validResource.modified = "not-a-date";
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
return this.RequestParser.parse(this.validRequest, this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback
|
||||
.calledWith(
|
||||
"resource modified date could not be understood: "+
|
||||
this.validResource.modified
|
||||
)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource with a valid date", function() {
|
||||
beforeEach(function() {
|
||||
this.date = "12:00 01/02/03";
|
||||
this.validResource.modified = this.date;
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
this.RequestParser.parse(this.validRequest, this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should return the date as a Javascript Date object", function() {
|
||||
(this.data.resources[0].modified instanceof Date).should.equal(true);
|
||||
return this.data.resources[0].modified.getTime().should.equal(Date.parse(this.date));
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource without either a content or URL attribute", function() {
|
||||
beforeEach(function() {
|
||||
delete this.validResource.url;
|
||||
delete this.validResource.content;
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
return this.RequestParser.parse(this.validRequest, this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("all resources should have either a url or content attribute")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource where the content is not a string", function() {
|
||||
beforeEach(function() {
|
||||
this.validResource.content = [];
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
return this.RequestParser.parse((this.validRequest), this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("content attribute should be a string")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource where the url is not a string", function() {
|
||||
beforeEach(function() {
|
||||
this.validResource.url = [];
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
return this.RequestParser.parse((this.validRequest), this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("url attribute should be a string")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource with a url", function() {
|
||||
beforeEach(function() {
|
||||
this.validResource.url = (this.url = "www.example.com");
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
this.RequestParser.parse((this.validRequest), this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should return the url in the parsed response", function() {
|
||||
return this.data.resources[0].url.should.equal(this.url);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a resource with a content attribute", function() {
|
||||
beforeEach(function() {
|
||||
this.validResource.content = (this.content = "Hello world");
|
||||
this.validRequest.compile.resources.push(this.validResource);
|
||||
this.RequestParser.parse((this.validRequest), this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should return the content in the parsed response", function() {
|
||||
return this.data.resources[0].content.should.equal(this.content);
|
||||
});
|
||||
});
|
||||
|
||||
describe("without a root resource path", function() {
|
||||
beforeEach(function() {
|
||||
delete this.validRequest.compile.rootResourcePath;
|
||||
this.RequestParser.parse((this.validRequest), this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should set the root resource path to 'main.tex' by default", function() {
|
||||
return this.data.rootResourcePath.should.equal("main.tex");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a root resource path", function() {
|
||||
beforeEach(function() {
|
||||
this.validRequest.compile.rootResourcePath = (this.path = "test.tex");
|
||||
this.RequestParser.parse((this.validRequest), this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should return the root resource path in the parsed response", function() {
|
||||
return this.data.rootResourcePath.should.equal(this.path);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a root resource path that is not a string", function() {
|
||||
beforeEach(function() {
|
||||
this.validRequest.compile.rootResourcePath = [];
|
||||
return this.RequestParser.parse((this.validRequest), this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("rootResourcePath attribute should be a string")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a root resource path that needs escaping", function() {
|
||||
beforeEach(function() {
|
||||
this.badPath = "`rm -rf foo`.tex";
|
||||
this.goodPath = "rm -rf foo.tex";
|
||||
this.validRequest.compile.rootResourcePath = this.badPath;
|
||||
this.validRequest.compile.resources.push({
|
||||
path: this.badPath,
|
||||
date: "12:00 01/02/03",
|
||||
content: "Hello world"
|
||||
});
|
||||
this.RequestParser.parse(this.validRequest, this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
it("should return the escaped resource", function() {
|
||||
return this.data.rootResourcePath.should.equal(this.goodPath);
|
||||
});
|
||||
|
||||
return it("should also escape the resource path", function() {
|
||||
return this.data.resources[0].path.should.equal(this.goodPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a root resource path that has a relative path", function() {
|
||||
beforeEach(function() {
|
||||
this.validRequest.compile.rootResourcePath = "foo/../../bar.tex";
|
||||
this.RequestParser.parse(this.validRequest, this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("relative path in root resource")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a root resource path that has unescaped + relative path", function() {
|
||||
beforeEach(function() {
|
||||
this.validRequest.compile.rootResourcePath = "foo/#../bar.tex";
|
||||
this.RequestParser.parse(this.validRequest, this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("relative path in root resource")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with an unknown syncType", function() {
|
||||
beforeEach(function() {
|
||||
this.validRequest.compile.options.syncType = "unexpected";
|
||||
this.RequestParser.parse(this.validRequest, this.callback);
|
||||
return this.data = this.callback.args[0][1];});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith("syncType attribute should be one of: full, incremental")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
153
test/unit/js/ResourceStateManagerTests.js
Normal file
153
test/unit/js/ResourceStateManagerTests.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
const should = require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/ResourceStateManager');
|
||||
const Path = require("path");
|
||||
const Errors = require("../../../app/js/Errors");
|
||||
|
||||
describe("ResourceStateManager", function() {
|
||||
beforeEach(function() {
|
||||
this.ResourceStateManager = SandboxedModule.require(modulePath, { requires: {
|
||||
"fs": (this.fs = {}),
|
||||
"logger-sharelatex": {log: sinon.stub(), err: sinon.stub()},
|
||||
"./SafeReader": (this.SafeReader = {})
|
||||
}
|
||||
});
|
||||
this.basePath = "/path/to/write/files/to";
|
||||
this.resources = [
|
||||
{path: "resource-1-mock"},
|
||||
{path: "resource-2-mock"},
|
||||
{path: "resource-3-mock"}
|
||||
];
|
||||
this.state = "1234567890";
|
||||
this.resourceFileName = `${this.basePath}/.project-sync-state`;
|
||||
this.resourceFileContents = `${this.resources[0].path}\n${this.resources[1].path}\n${this.resources[2].path}\nstateHash:${this.state}`;
|
||||
return this.callback = sinon.stub();
|
||||
});
|
||||
|
||||
describe("saveProjectState", function() {
|
||||
beforeEach(function() {
|
||||
return this.fs.writeFile = sinon.stub().callsArg(2);
|
||||
});
|
||||
|
||||
describe("when the state is specified", function() {
|
||||
beforeEach(function() {
|
||||
return this.ResourceStateManager.saveProjectState(this.state, this.resources, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should write the resource list to disk", function() {
|
||||
return this.fs.writeFile
|
||||
.calledWith(this.resourceFileName, this.resourceFileContents)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the state is undefined", function() {
|
||||
beforeEach(function() {
|
||||
this.state = undefined;
|
||||
this.fs.unlink = sinon.stub().callsArg(1);
|
||||
return this.ResourceStateManager.saveProjectState(this.state, this.resources, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should unlink the resource file", function() {
|
||||
return this.fs.unlink
|
||||
.calledWith(this.resourceFileName)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should not write the resource list to disk", function() {
|
||||
return this.fs.writeFile.called.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkProjectStateMatches", function() {
|
||||
|
||||
describe("when the state matches", function() {
|
||||
beforeEach(function() {
|
||||
this.SafeReader.readFile = sinon.stub().callsArgWith(3, null, this.resourceFileContents);
|
||||
return this.ResourceStateManager.checkProjectStateMatches(this.state, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should read the resource file", function() {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(this.resourceFileName)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with the results", function() {
|
||||
return this.callback.calledWithMatch(null, this.resources).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the state does not match", function() {
|
||||
beforeEach(function() {
|
||||
this.SafeReader.readFile = sinon.stub().callsArgWith(3, null, this.resourceFileContents);
|
||||
return this.ResourceStateManager.checkProjectStateMatches("not-the-original-state", this.basePath, this.callback);
|
||||
});
|
||||
|
||||
return it("should call the callback with an error", function() {
|
||||
const error = new Errors.FilesOutOfSyncError("invalid state for incremental update");
|
||||
return this.callback.calledWith(error).should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return describe("checkResourceFiles", function() {
|
||||
describe("when all the files are present", function() {
|
||||
beforeEach(function() {
|
||||
this.allFiles = [ this.resources[0].path, this.resources[1].path, this.resources[2].path];
|
||||
return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.calledWithExactly().should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a missing file", function() {
|
||||
beforeEach(function() {
|
||||
this.allFiles = [ this.resources[0].path, this.resources[1].path];
|
||||
this.fs.stat = sinon.stub().callsArgWith(1, new Error());
|
||||
return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
return it("should call the callback with an error", function() {
|
||||
const error = new Errors.FilesOutOfSyncError("resource files missing in incremental update");
|
||||
return this.callback.calledWith(error).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when a resource contains a relative path", function() {
|
||||
beforeEach(function() {
|
||||
this.resources[0].path = "../foo/bar.tex";
|
||||
this.allFiles = [ this.resources[0].path, this.resources[1].path, this.resources[2].path];
|
||||
return this.ResourceStateManager.checkResourceFiles(this.resources, this.allFiles, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
return it("should call the callback with an error", function() {
|
||||
return this.callback.calledWith(new Error("relative path in resource file list")).should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
414
test/unit/js/ResourceWriterTests.js
Normal file
414
test/unit/js/ResourceWriterTests.js
Normal file
@@ -0,0 +1,414 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS206: Consider reworking classes to avoid initClass
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
const should = require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/ResourceWriter');
|
||||
const path = require("path");
|
||||
|
||||
describe("ResourceWriter", function() {
|
||||
beforeEach(function() {
|
||||
let Timer;
|
||||
this.ResourceWriter = SandboxedModule.require(modulePath, { requires: {
|
||||
"fs": (this.fs = {
|
||||
mkdir: sinon.stub().callsArg(1),
|
||||
unlink: sinon.stub().callsArg(1)
|
||||
}),
|
||||
"./ResourceStateManager": (this.ResourceStateManager = {}),
|
||||
"wrench": (this.wrench = {}),
|
||||
"./UrlCache" : (this.UrlCache = {}),
|
||||
"mkdirp" : (this.mkdirp = sinon.stub().callsArg(1)),
|
||||
"./OutputFileFinder": (this.OutputFileFinder = {}),
|
||||
"logger-sharelatex": {log: sinon.stub(), err: sinon.stub()},
|
||||
"./Metrics": (this.Metrics = {
|
||||
Timer: (Timer = (function() {
|
||||
Timer = class Timer {
|
||||
static initClass() {
|
||||
this.prototype.done = sinon.stub();
|
||||
}
|
||||
};
|
||||
Timer.initClass();
|
||||
return Timer;
|
||||
})())
|
||||
})
|
||||
}
|
||||
}
|
||||
);
|
||||
this.project_id = "project-id-123";
|
||||
this.basePath = "/path/to/write/files/to";
|
||||
return this.callback = sinon.stub();
|
||||
});
|
||||
|
||||
describe("syncResourcesToDisk on a full request", function() {
|
||||
beforeEach(function() {
|
||||
this.resources = [
|
||||
"resource-1-mock",
|
||||
"resource-2-mock",
|
||||
"resource-3-mock"
|
||||
];
|
||||
this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3);
|
||||
this.ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2);
|
||||
this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3);
|
||||
return this.ResourceWriter.syncResourcesToDisk({
|
||||
project_id: this.project_id,
|
||||
syncState: (this.syncState = "0123456789abcdef"),
|
||||
resources: this.resources
|
||||
}, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should remove old files", function() {
|
||||
return this.ResourceWriter._removeExtraneousFiles
|
||||
.calledWith(this.resources, this.basePath)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should write each resource to disk", function() {
|
||||
return Array.from(this.resources).map((resource) =>
|
||||
this.ResourceWriter._writeResourceToDisk
|
||||
.calledWith(this.project_id, resource, this.basePath)
|
||||
.should.equal(true));
|
||||
});
|
||||
|
||||
it("should store the sync state and resource list", function() {
|
||||
return this.ResourceStateManager.saveProjectState
|
||||
.calledWith(this.syncState, this.resources, this.basePath)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncResourcesToDisk on an incremental update", function() {
|
||||
beforeEach(function() {
|
||||
this.resources = [
|
||||
"resource-1-mock"
|
||||
];
|
||||
this.ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3);
|
||||
this.ResourceWriter._removeExtraneousFiles = sinon.stub().callsArgWith(2, null, (this.outputFiles = []), (this.allFiles = []));
|
||||
this.ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, null, this.resources);
|
||||
this.ResourceStateManager.saveProjectState = sinon.stub().callsArg(3);
|
||||
this.ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3);
|
||||
return this.ResourceWriter.syncResourcesToDisk({
|
||||
project_id: this.project_id,
|
||||
syncType: "incremental",
|
||||
syncState: (this.syncState = "1234567890abcdef"),
|
||||
resources: this.resources
|
||||
}, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should check the sync state matches", function() {
|
||||
return this.ResourceStateManager.checkProjectStateMatches
|
||||
.calledWith(this.syncState, this.basePath)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should remove old files", function() {
|
||||
return this.ResourceWriter._removeExtraneousFiles
|
||||
.calledWith(this.resources, this.basePath)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should check each resource exists", function() {
|
||||
return this.ResourceStateManager.checkResourceFiles
|
||||
.calledWith(this.resources, this.allFiles, this.basePath)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should write each resource to disk", function() {
|
||||
return Array.from(this.resources).map((resource) =>
|
||||
this.ResourceWriter._writeResourceToDisk
|
||||
.calledWith(this.project_id, resource, this.basePath)
|
||||
.should.equal(true));
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncResourcesToDisk on an incremental update when the state does not match", function() {
|
||||
beforeEach(function() {
|
||||
this.resources = [
|
||||
"resource-1-mock"
|
||||
];
|
||||
this.ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, (this.error = new Error()));
|
||||
return this.ResourceWriter.syncResourcesToDisk({
|
||||
project_id: this.project_id,
|
||||
syncType: "incremental",
|
||||
syncState: (this.syncState = "1234567890abcdef"),
|
||||
resources: this.resources
|
||||
}, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should check whether the sync state matches", function() {
|
||||
return this.ResourceStateManager.checkProjectStateMatches
|
||||
.calledWith(this.syncState, this.basePath)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with an error", function() {
|
||||
return this.callback.calledWith(this.error).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("_removeExtraneousFiles", function() {
|
||||
beforeEach(function() {
|
||||
this.output_files = [{
|
||||
path: "output.pdf",
|
||||
type: "pdf"
|
||||
}, {
|
||||
path: "extra/file.tex",
|
||||
type: "tex"
|
||||
}, {
|
||||
path: "extra.aux",
|
||||
type: "aux"
|
||||
}, {
|
||||
path: "cache/_chunk1"
|
||||
},{
|
||||
path: "figures/image-eps-converted-to.pdf",
|
||||
type: "pdf"
|
||||
},{
|
||||
path: "foo/main-figure0.md5",
|
||||
type: "md5"
|
||||
}, {
|
||||
path: "foo/main-figure0.dpth",
|
||||
type: "dpth"
|
||||
}, {
|
||||
path: "foo/main-figure0.pdf",
|
||||
type: "pdf"
|
||||
}, {
|
||||
path: "_minted-main/default-pyg-prefix.pygstyle",
|
||||
type: "pygstyle"
|
||||
}, {
|
||||
path: "_minted-main/default.pygstyle",
|
||||
type: "pygstyle"
|
||||
}, {
|
||||
path: "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex",
|
||||
type: "pygtex"
|
||||
}, {
|
||||
path: "_markdown_main/30893013dec5d869a415610079774c2f.md.tex",
|
||||
type: "tex"
|
||||
}];
|
||||
this.resources = "mock-resources";
|
||||
this.OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, this.output_files);
|
||||
this.ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1);
|
||||
return this.ResourceWriter._removeExtraneousFiles(this.resources, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should find the existing output files", function() {
|
||||
return this.OutputFileFinder.findOutputFiles
|
||||
.calledWith(this.resources, this.basePath)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should delete the output files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "output.pdf"))
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should delete the extra files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "extra/file.tex"))
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should not delete the extra aux files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "extra.aux"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the knitr cache file", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "cache/_chunk1"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the epstopdf converted files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "figures/image-eps-converted-to.pdf"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the tikz md5 files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "foo/main-figure0.md5"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the tikz dpth files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "foo/main-figure0.dpth"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the tikz pdf files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "foo/main-figure0.pdf"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the minted pygstyle files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "_minted-main/default-pyg-prefix.pygstyle"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the minted default pygstyle files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "_minted-main/default.pygstyle"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the minted default pygtex files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should not delete the markdown md.tex files", function() {
|
||||
return this.ResourceWriter._deleteFileIfNotDirectory
|
||||
.calledWith(path.join(this.basePath, "_markdown_main/30893013dec5d869a415610079774c2f.md.tex"))
|
||||
.should.equal(false);
|
||||
});
|
||||
|
||||
it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should time the request", function() {
|
||||
return this.Metrics.Timer.prototype.done.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("_writeResourceToDisk", function() {
|
||||
describe("with a url based resource", function() {
|
||||
beforeEach(function() {
|
||||
this.resource = {
|
||||
path: "main.tex",
|
||||
url: "http://www.example.com/main.tex",
|
||||
modified: Date.now()
|
||||
};
|
||||
this.UrlCache.downloadUrlToFile = sinon.stub().callsArgWith(4, "fake error downloading file");
|
||||
return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should ensure the directory exists", function() {
|
||||
return this.mkdirp
|
||||
.calledWith(path.dirname(path.join(this.basePath, this.resource.path)))
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should write the URL from the cache", function() {
|
||||
return this.UrlCache.downloadUrlToFile
|
||||
.calledWith(this.project_id, this.resource.url, path.join(this.basePath, this.resource.path), this.resource.modified)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should not return an error if the resource writer errored", function() {
|
||||
return should.not.exist(this.callback.args[0][0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a content based resource", function() {
|
||||
beforeEach(function() {
|
||||
this.resource = {
|
||||
path: "main.tex",
|
||||
content: "Hello world"
|
||||
};
|
||||
this.fs.writeFile = sinon.stub().callsArg(2);
|
||||
return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should ensure the directory exists", function() {
|
||||
return this.mkdirp
|
||||
.calledWith(path.dirname(path.join(this.basePath, this.resource.path)))
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should write the contents to disk", function() {
|
||||
return this.fs.writeFile
|
||||
.calledWith(path.join(this.basePath, this.resource.path), this.resource.content)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with a file path that breaks out of the root folder", function() {
|
||||
beforeEach(function() {
|
||||
this.resource = {
|
||||
path: "../../main.tex",
|
||||
content: "Hello world"
|
||||
};
|
||||
this.fs.writeFile = sinon.stub().callsArg(2);
|
||||
return this.ResourceWriter._writeResourceToDisk(this.project_id, this.resource, this.basePath, this.callback);
|
||||
});
|
||||
|
||||
it("should not write to disk", function() {
|
||||
return this.fs.writeFile.called.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback
|
||||
.calledWith(new Error("resource path is outside root directory"))
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return describe("checkPath", function() {
|
||||
describe("with a valid path", function() {
|
||||
beforeEach(function() {
|
||||
return this.ResourceWriter.checkPath("foo", "bar", this.callback);
|
||||
});
|
||||
|
||||
return it("should return the joined path", function() {
|
||||
return this.callback.calledWith(null, "foo/bar")
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with an invalid path", function() {
|
||||
beforeEach(function() {
|
||||
return this.ResourceWriter.checkPath("foo", "baz/../../bar", this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith(new Error("resource path is outside root directory"))
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with another invalid path matching on a prefix", function() {
|
||||
beforeEach(function() {
|
||||
return this.ResourceWriter.checkPath("foo", "../foobar/baz", this.callback);
|
||||
});
|
||||
|
||||
return it("should return an error", function() {
|
||||
return this.callback.calledWith(new Error("resource path is outside root directory"))
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
225
test/unit/js/StaticServerForbidSymlinksTests.js
Normal file
225
test/unit/js/StaticServerForbidSymlinksTests.js
Normal file
@@ -0,0 +1,225 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const should = require('chai').should();
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const sinon = require('sinon');
|
||||
const modulePath = path.join(__dirname, "../../../app/js/StaticServerForbidSymlinks");
|
||||
const { expect } = require("chai");
|
||||
|
||||
describe("StaticServerForbidSymlinks", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
this.settings = {
|
||||
path: {
|
||||
compilesDir: "/compiles/here"
|
||||
}
|
||||
};
|
||||
|
||||
this.fs = {};
|
||||
this.ForbidSymlinks = SandboxedModule.require(modulePath, { requires: {
|
||||
"settings-sharelatex":this.settings,
|
||||
"logger-sharelatex": {
|
||||
log() {},
|
||||
warn() {},
|
||||
error() {}
|
||||
},
|
||||
"fs":this.fs
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.dummyStatic = (rootDir, options) =>
|
||||
(req, res, next) =>
|
||||
// console.log "dummyStatic serving file", rootDir, "called with", req.url
|
||||
// serve it
|
||||
next()
|
||||
|
||||
;
|
||||
|
||||
this.StaticServerForbidSymlinks = this.ForbidSymlinks(this.dummyStatic, this.settings.path.compilesDir);
|
||||
this.req = {
|
||||
params: {
|
||||
project_id:"12345"
|
||||
}
|
||||
};
|
||||
|
||||
this.res = {};
|
||||
return this.req.url = "/12345/output.pdf";
|
||||
});
|
||||
|
||||
|
||||
describe("sending a normal file through", function() {
|
||||
beforeEach(function() {
|
||||
return this.fs.realpath = sinon.stub().callsArgWith(1, null, `${this.settings.path.compilesDir}/${this.req.params.project_id}/output.pdf`);
|
||||
});
|
||||
|
||||
return it("should call next", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(200);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res, done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("with a missing file", function() {
|
||||
beforeEach(function() {
|
||||
return this.fs.realpath = sinon.stub().callsArgWith(1, {code: 'ENOENT'}, `${this.settings.path.compilesDir}/${this.req.params.project_id}/unknown.pdf`);
|
||||
});
|
||||
|
||||
return it("should send a 404", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(404);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("with a symlink file", function() {
|
||||
beforeEach(function() {
|
||||
return this.fs.realpath = sinon.stub().callsArgWith(1, null, `/etc/${this.req.params.project_id}/output.pdf`);
|
||||
});
|
||||
|
||||
return it("should send a 404", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(404);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("with a relative file", function() {
|
||||
beforeEach(function() {
|
||||
return this.req.url = "/12345/../67890/output.pdf";
|
||||
});
|
||||
|
||||
return it("should send a 404", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(404);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("with a unnormalized file containing .", function() {
|
||||
beforeEach(function() {
|
||||
return this.req.url = "/12345/foo/./output.pdf";
|
||||
});
|
||||
|
||||
return it("should send a 404", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(404);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("with a file containing an empty path", function() {
|
||||
beforeEach(function() {
|
||||
return this.req.url = "/12345/foo//output.pdf";
|
||||
});
|
||||
|
||||
return it("should send a 404", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(404);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a non-project file", function() {
|
||||
beforeEach(function() {
|
||||
return this.req.url = "/.foo/output.pdf";
|
||||
});
|
||||
|
||||
return it("should send a 404", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(404);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a file outside the compiledir", function() {
|
||||
beforeEach(function() {
|
||||
return this.req.url = "/../bar/output.pdf";
|
||||
});
|
||||
|
||||
return it("should send a 404", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(404);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("with a file with no leading /", function() {
|
||||
beforeEach(function() {
|
||||
return this.req.url = "./../bar/output.pdf";
|
||||
});
|
||||
|
||||
return it("should send a 404", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(404);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with a github style path", function() {
|
||||
beforeEach(function() {
|
||||
this.req.url = "/henryoswald-latex_example/output/output.log";
|
||||
return this.fs.realpath = sinon.stub().callsArgWith(1, null, `${this.settings.path.compilesDir}/henryoswald-latex_example/output/output.log`);
|
||||
});
|
||||
|
||||
return it("should call next", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(200);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res, done);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with an error from fs.realpath", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
return this.fs.realpath = sinon.stub().callsArgWith(1, "error");
|
||||
});
|
||||
|
||||
return it("should send a 500", function(done){
|
||||
this.res.sendStatus = function(resCode){
|
||||
resCode.should.equal(500);
|
||||
return done();
|
||||
};
|
||||
return this.StaticServerForbidSymlinks(this.req, this.res);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
155
test/unit/js/TikzManager.js
Normal file
155
test/unit/js/TikzManager.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/TikzManager');
|
||||
|
||||
describe('TikzManager', function() {
|
||||
beforeEach(function() {
|
||||
return this.TikzManager = SandboxedModule.require(modulePath, { requires: {
|
||||
"./ResourceWriter": (this.ResourceWriter = {}),
|
||||
"./SafeReader": (this.SafeReader = {}),
|
||||
"fs": (this.fs = {}),
|
||||
"logger-sharelatex": (this.logger = {log() {}})
|
||||
}
|
||||
});});
|
||||
|
||||
describe("checkMainFile", function() {
|
||||
beforeEach(function() {
|
||||
this.compileDir = "compile-dir";
|
||||
this.mainFile = "main.tex";
|
||||
return this.callback = sinon.stub();
|
||||
});
|
||||
|
||||
describe("if there is already an output.tex file in the resources", function() {
|
||||
beforeEach(function() {
|
||||
this.resources = [{path:"main.tex"},{path:"output.tex"}];
|
||||
return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback);
|
||||
});
|
||||
|
||||
return it("should call the callback with false ", function() {
|
||||
return this.callback.calledWithExactly(null, false)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("if there is no output.tex file in the resources", function() {
|
||||
beforeEach(function() {
|
||||
this.resources = [{path:"main.tex"}];
|
||||
return this.ResourceWriter.checkPath = sinon.stub()
|
||||
.withArgs(this.compileDir, this.mainFile)
|
||||
.callsArgWith(2, null, `${this.compileDir}/${this.mainFile}`);
|
||||
});
|
||||
|
||||
describe("and the main file contains tikzexternalize", function() {
|
||||
beforeEach(function() {
|
||||
this.SafeReader.readFile = sinon.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, "hello \\tikzexternalize");
|
||||
return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback);
|
||||
});
|
||||
|
||||
it("should look at the file on disk", function() {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with true ", function() {
|
||||
return this.callback.calledWithExactly(null, true)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("and the main file does not contain tikzexternalize", function() {
|
||||
beforeEach(function() {
|
||||
this.SafeReader.readFile = sinon.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, "hello");
|
||||
return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback);
|
||||
});
|
||||
|
||||
it("should look at the file on disk", function() {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with false", function() {
|
||||
return this.callback.calledWithExactly(null, false)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("and the main file contains \\usepackage{pstool}", function() {
|
||||
beforeEach(function() {
|
||||
this.SafeReader.readFile = sinon.stub()
|
||||
.withArgs(`${this.compileDir}/${this.mainFile}`)
|
||||
.callsArgWith(3, null, "hello \\usepackage[random-options]{pstool}");
|
||||
return this.TikzManager.checkMainFile(this.compileDir, this.mainFile, this.resources, this.callback);
|
||||
});
|
||||
|
||||
it("should look at the file on disk", function() {
|
||||
return this.SafeReader.readFile
|
||||
.calledWith(`${this.compileDir}/${this.mainFile}`)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback with true ", function() {
|
||||
return this.callback.calledWithExactly(null, true)
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return describe("injectOutputFile", function() {
|
||||
beforeEach(function() {
|
||||
this.rootDir = "/mock";
|
||||
this.filename = "filename.tex";
|
||||
this.callback = sinon.stub();
|
||||
this.content = `\
|
||||
\\documentclass{article}
|
||||
\\usepackage{tikz}
|
||||
\\tikzexternalize
|
||||
\\begin{document}
|
||||
Hello world
|
||||
\\end{document}\
|
||||
`;
|
||||
this.fs.readFile = sinon.stub().callsArgWith(2, null, this.content);
|
||||
this.fs.writeFile = sinon.stub().callsArg(3);
|
||||
this.ResourceWriter.checkPath = sinon.stub().callsArgWith(2, null, `${this.rootDir}/${this.filename}`);
|
||||
return this.TikzManager.injectOutputFile(this.rootDir, this.filename, this.callback);
|
||||
});
|
||||
|
||||
it("sould check the path", function() {
|
||||
return this.ResourceWriter.checkPath.calledWith(this.rootDir, this.filename)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should read the file", function() {
|
||||
return this.fs.readFile
|
||||
.calledWith(`${this.rootDir}/${this.filename}`, "utf8")
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should write out the same file as output.tex", function() {
|
||||
return this.fs.writeFile
|
||||
.calledWith(`${this.rootDir}/output.tex`, this.content, {flag: 'wx'})
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
268
test/unit/js/UrlCacheTests.js
Normal file
268
test/unit/js/UrlCacheTests.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS101: Remove unnecessary use of Array.from
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/UrlCache');
|
||||
const { EventEmitter } = require("events");
|
||||
|
||||
describe("UrlCache", function() {
|
||||
beforeEach(function() {
|
||||
this.callback = sinon.stub();
|
||||
this.url = "www.example.com/file";
|
||||
this.project_id = "project-id-123";
|
||||
return this.UrlCache = SandboxedModule.require(modulePath, { requires: {
|
||||
"./db" : {},
|
||||
"./UrlFetcher" : (this.UrlFetcher = {}),
|
||||
"logger-sharelatex": (this.logger = {log: sinon.stub()}),
|
||||
"settings-sharelatex": (this.Settings = { path: {clsiCacheDir: "/cache/dir"} }),
|
||||
"fs": (this.fs = {})
|
||||
}
|
||||
});});
|
||||
|
||||
describe("_doesUrlNeedDownloading", function() {
|
||||
beforeEach(function() {
|
||||
this.lastModified = new Date();
|
||||
return this.lastModifiedRoundedToSeconds = new Date(Math.floor(this.lastModified.getTime() / 1000) * 1000);
|
||||
});
|
||||
|
||||
describe("when URL does not exist in cache", function() {
|
||||
beforeEach(function() {
|
||||
this.UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, null);
|
||||
return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
return it("should return the callback with true", function() {
|
||||
return this.callback.calledWith(null, true).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when URL does exist in cache", function() {
|
||||
beforeEach(function() {
|
||||
this.urlDetails = {};
|
||||
return this.UrlCache._findUrlDetails = sinon.stub().callsArgWith(2, null, this.urlDetails);
|
||||
});
|
||||
|
||||
describe("when the modified date is more recent than the cached modified date", function() {
|
||||
beforeEach(function() {
|
||||
this.urlDetails.lastModified = new Date(this.lastModified.getTime() - 1000);
|
||||
return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
it("should get the url details", function() {
|
||||
return this.UrlCache._findUrlDetails
|
||||
.calledWith(this.project_id, this.url)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should return the callback with true", function() {
|
||||
return this.callback.calledWith(null, true).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the cached modified date is more recent than the modified date", function() {
|
||||
beforeEach(function() {
|
||||
this.urlDetails.lastModified = new Date(this.lastModified.getTime() + 1000);
|
||||
return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
return it("should return the callback with false", function() {
|
||||
return this.callback.calledWith(null, false).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the cached modified date is equal to the modified date", function() {
|
||||
beforeEach(function() {
|
||||
this.urlDetails.lastModified = this.lastModified;
|
||||
return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
return it("should return the callback with false", function() {
|
||||
return this.callback.calledWith(null, false).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the provided modified date does not exist", function() {
|
||||
beforeEach(function() {
|
||||
this.lastModified = null;
|
||||
return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
return it("should return the callback with true", function() {
|
||||
return this.callback.calledWith(null, true).should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the URL does not have a modified date", function() {
|
||||
beforeEach(function() {
|
||||
this.urlDetails.lastModified = null;
|
||||
return this.UrlCache._doesUrlNeedDownloading(this.project_id, this.url, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
return it("should return the callback with true", function() {
|
||||
return this.callback.calledWith(null, true).should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("_ensureUrlIsInCache", function() {
|
||||
beforeEach(function() {
|
||||
this.UrlFetcher.pipeUrlToFile = sinon.stub().callsArg(2);
|
||||
return this.UrlCache._updateOrCreateUrlDetails = sinon.stub().callsArg(3);
|
||||
});
|
||||
|
||||
describe("when the URL needs updating", function() {
|
||||
beforeEach(function() {
|
||||
this.UrlCache._doesUrlNeedDownloading = sinon.stub().callsArgWith(3, null, true);
|
||||
return this.UrlCache._ensureUrlIsInCache(this.project_id, this.url, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
it("should check that the url needs downloading", function() {
|
||||
return this.UrlCache._doesUrlNeedDownloading
|
||||
.calledWith(this.project_id, this.url, this.lastModifiedRoundedToSeconds)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should download the URL to the cache file", function() {
|
||||
return this.UrlFetcher.pipeUrlToFile
|
||||
.calledWith(this.url, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url))
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
|
||||
it("should update the database entry", function() {
|
||||
return this.UrlCache._updateOrCreateUrlDetails
|
||||
.calledWith(this.project_id, this.url, this.lastModifiedRoundedToSeconds)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should return the callback with the cache file path", function() {
|
||||
return this.callback
|
||||
.calledWith(null, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url))
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("when the URL does not need updating", function() {
|
||||
beforeEach(function() {
|
||||
this.UrlCache._doesUrlNeedDownloading = sinon.stub().callsArgWith(3, null, false);
|
||||
return this.UrlCache._ensureUrlIsInCache(this.project_id, this.url, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
it("should not download the URL to the cache file", function() {
|
||||
return this.UrlFetcher.pipeUrlToFile
|
||||
.called.should.equal(false);
|
||||
});
|
||||
|
||||
return it("should return the callback with the cache file path", function() {
|
||||
return this.callback
|
||||
.calledWith(null, this.UrlCache._cacheFilePathForUrl(this.project_id, this.url))
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadUrlToFile", function() {
|
||||
beforeEach(function() {
|
||||
this.cachePath = "path/to/cached/url";
|
||||
this.destPath = "path/to/destination";
|
||||
this.UrlCache._copyFile = sinon.stub().callsArg(2);
|
||||
this.UrlCache._ensureUrlIsInCache = sinon.stub().callsArgWith(3, null, this.cachePath);
|
||||
return this.UrlCache.downloadUrlToFile(this.project_id, this.url, this.destPath, this.lastModified, this.callback);
|
||||
});
|
||||
|
||||
it("should ensure the URL is downloaded and updated in the cache", function() {
|
||||
return this.UrlCache._ensureUrlIsInCache
|
||||
.calledWith(this.project_id, this.url, this.lastModified)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should copy the file to the new location", function() {
|
||||
return this.UrlCache._copyFile
|
||||
.calledWith(this.cachePath, this.destPath)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("_deleteUrlCacheFromDisk", function() {
|
||||
beforeEach(function() {
|
||||
this.fs.unlink = sinon.stub().callsArg(1);
|
||||
return this.UrlCache._deleteUrlCacheFromDisk(this.project_id, this.url, this.callback);
|
||||
});
|
||||
|
||||
it("should delete the cache file", function() {
|
||||
return this.fs.unlink
|
||||
.calledWith(this.UrlCache._cacheFilePathForUrl(this.project_id, this.url))
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("_clearUrlFromCache", function() {
|
||||
beforeEach(function() {
|
||||
this.UrlCache._deleteUrlCacheFromDisk = sinon.stub().callsArg(2);
|
||||
this.UrlCache._clearUrlDetails = sinon.stub().callsArg(2);
|
||||
return this.UrlCache._clearUrlFromCache(this.project_id, this.url, this.callback);
|
||||
});
|
||||
|
||||
it("should delete the file on disk", function() {
|
||||
return this.UrlCache._deleteUrlCacheFromDisk
|
||||
.calledWith(this.project_id, this.url)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should clear the entry in the database", function() {
|
||||
return this.UrlCache._clearUrlDetails
|
||||
.calledWith(this.project_id, this.url)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("clearProject", function() {
|
||||
beforeEach(function() {
|
||||
this.urls = [
|
||||
"www.example.com/file1",
|
||||
"www.example.com/file2"
|
||||
];
|
||||
this.UrlCache._findAllUrlsInProject = sinon.stub().callsArgWith(1, null, this.urls);
|
||||
this.UrlCache._clearUrlFromCache = sinon.stub().callsArg(2);
|
||||
return this.UrlCache.clearProject(this.project_id, this.callback);
|
||||
});
|
||||
|
||||
it("should clear the cache for each url in the project", function() {
|
||||
return Array.from(this.urls).map((url) =>
|
||||
this.UrlCache._clearUrlFromCache
|
||||
.calledWith(this.project_id, url)
|
||||
.should.equal(true));
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
159
test/unit/js/UrlFetcherTests.js
Normal file
159
test/unit/js/UrlFetcherTests.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const SandboxedModule = require('sandboxed-module');
|
||||
const sinon = require('sinon');
|
||||
require('chai').should();
|
||||
const modulePath = require('path').join(__dirname, '../../../app/js/UrlFetcher');
|
||||
const { EventEmitter } = require("events");
|
||||
|
||||
describe("UrlFetcher", function() {
|
||||
beforeEach(function() {
|
||||
this.callback = sinon.stub();
|
||||
this.url = "https://www.example.com/file/here?query=string";
|
||||
return this.UrlFetcher = SandboxedModule.require(modulePath, { requires: {
|
||||
request: { defaults: (this.defaults = sinon.stub().returns(this.request = {}))
|
||||
},
|
||||
fs: (this.fs = {}),
|
||||
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
|
||||
"settings-sharelatex": (this.settings = {})
|
||||
}
|
||||
});});
|
||||
|
||||
it("should turn off the cookie jar in request", function() {
|
||||
return this.defaults.calledWith({jar: false})
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
describe("rewrite url domain if filestoreDomainOveride is set", function() {
|
||||
beforeEach(function() {
|
||||
this.path = "/path/to/file/on/disk";
|
||||
this.request.get = sinon.stub().returns(this.urlStream = new EventEmitter);
|
||||
this.urlStream.pipe = sinon.stub();
|
||||
this.urlStream.pause = sinon.stub();
|
||||
this.urlStream.resume = sinon.stub();
|
||||
this.fs.createWriteStream = sinon.stub().returns(this.fileStream = new EventEmitter);
|
||||
return this.fs.unlink = (file, callback) => callback();
|
||||
});
|
||||
|
||||
it("should use the normal domain when override not set", function(done){
|
||||
this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => {
|
||||
this.request.get.args[0][0].url.should.equal(this.url);
|
||||
return done();
|
||||
});
|
||||
this.res = {statusCode: 200};
|
||||
this.urlStream.emit("response", this.res);
|
||||
this.urlStream.emit("end");
|
||||
return this.fileStream.emit("finish");
|
||||
});
|
||||
|
||||
|
||||
return it("should use override domain when filestoreDomainOveride is set", function(done){
|
||||
this.settings.filestoreDomainOveride = "192.11.11.11";
|
||||
this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => {
|
||||
this.request.get.args[0][0].url.should.equal("192.11.11.11/file/here?query=string");
|
||||
return done();
|
||||
});
|
||||
this.res = {statusCode: 200};
|
||||
this.urlStream.emit("response", this.res);
|
||||
this.urlStream.emit("end");
|
||||
return this.fileStream.emit("finish");
|
||||
});
|
||||
});
|
||||
|
||||
return describe("pipeUrlToFile", function() {
|
||||
beforeEach(function(done){
|
||||
this.path = "/path/to/file/on/disk";
|
||||
this.request.get = sinon.stub().returns(this.urlStream = new EventEmitter);
|
||||
this.urlStream.pipe = sinon.stub();
|
||||
this.urlStream.pause = sinon.stub();
|
||||
this.urlStream.resume = sinon.stub();
|
||||
this.fs.createWriteStream = sinon.stub().returns(this.fileStream = new EventEmitter);
|
||||
this.fs.unlink = (file, callback) => callback();
|
||||
return done();
|
||||
});
|
||||
|
||||
describe("successfully", function() {
|
||||
beforeEach(function(done){
|
||||
this.UrlFetcher.pipeUrlToFile(this.url, this.path, () => {
|
||||
this.callback();
|
||||
return done();
|
||||
});
|
||||
this.res = {statusCode: 200};
|
||||
this.urlStream.emit("response", this.res);
|
||||
this.urlStream.emit("end");
|
||||
return this.fileStream.emit("finish");
|
||||
});
|
||||
|
||||
|
||||
it("should request the URL", function() {
|
||||
return this.request.get
|
||||
.calledWith(sinon.match({"url": this.url}))
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should open the file for writing", function() {
|
||||
return this.fs.createWriteStream
|
||||
.calledWith(this.path)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
it("should pipe the URL to the file", function() {
|
||||
return this.urlStream.pipe
|
||||
.calledWith(this.fileStream)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should call the callback", function() {
|
||||
return this.callback.called.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with non success status code", function() {
|
||||
beforeEach(function(done){
|
||||
this.UrlFetcher.pipeUrlToFile(this.url, this.path, err=> {
|
||||
this.callback(err);
|
||||
return done();
|
||||
});
|
||||
this.res = {statusCode: 404};
|
||||
this.urlStream.emit("response", this.res);
|
||||
return this.urlStream.emit("end");
|
||||
});
|
||||
|
||||
return it("should call the callback with an error", function() {
|
||||
return this.callback
|
||||
.calledWith(new Error("URL returned non-success status code: 404"))
|
||||
.should.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
return describe("with error", function() {
|
||||
beforeEach(function(done){
|
||||
this.UrlFetcher.pipeUrlToFile(this.url, this.path, err=> {
|
||||
this.callback(err);
|
||||
return done();
|
||||
});
|
||||
return this.urlStream.emit("error", (this.error = new Error("something went wrong")));
|
||||
});
|
||||
|
||||
it("should call the callback with the error", function() {
|
||||
return this.callback
|
||||
.calledWith(this.error)
|
||||
.should.equal(true);
|
||||
});
|
||||
|
||||
return it("should only call the callback once, even if end is called", function() {
|
||||
this.urlStream.emit("end");
|
||||
return this.callback.calledOnce.should.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user