ClsiManagerTests.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026
  1. const sinon = require('sinon')
  2. const chai = require('chai')
  3. const { expect } = chai
  4. const modulePath = '../../../../app/src/Features/Compile/ClsiManager.js'
  5. const SandboxedModule = require('sandboxed-module')
  6. describe('ClsiManager', function() {
  7. beforeEach(function() {
  8. this.jar = { cookie: 'stuff' }
  9. this.ClsiCookieManager = {
  10. getCookieJar: sinon.stub().callsArgWith(1, null, this.jar),
  11. setServerId: sinon.stub().callsArgWith(2),
  12. _getServerId: sinon.stub()
  13. }
  14. this.ClsiStateManager = {
  15. computeHash: sinon.stub().callsArgWith(2, null, '01234567890abcdef')
  16. }
  17. this.ClsiFormatChecker = {
  18. checkRecoursesForProblems: sinon.stub().callsArgWith(1)
  19. }
  20. this.Project = {}
  21. this.ProjectEntityHandler = {}
  22. this.ProjectGetter = {}
  23. this.DocumentUpdaterHandler = {
  24. getProjectDocsIfMatch: sinon.stub().callsArgWith(2, null, null)
  25. }
  26. this.ProjectRootDocManager = {
  27. setRootDocAutomatically: sinon.stub().yields()
  28. }
  29. this.logger = {
  30. log: sinon.stub(),
  31. error: sinon.stub(),
  32. err: sinon.stub(),
  33. warn: sinon.stub()
  34. }
  35. this.request = sinon.stub()
  36. this.Metrics = {
  37. Timer: class Metrics {
  38. constructor() {
  39. this.done = sinon.stub()
  40. }
  41. },
  42. inc: sinon.stub()
  43. }
  44. this.ClsiManager = SandboxedModule.require(modulePath, {
  45. globals: {
  46. console: console
  47. },
  48. requires: {
  49. 'settings-sharelatex': (this.settings = {
  50. apis: {
  51. filestore: {
  52. url: 'filestore.example.com',
  53. secret: 'secret'
  54. },
  55. clsi: {
  56. url: 'http://clsi.example.com'
  57. },
  58. clsi_priority: {
  59. url: 'https://clsipremium.example.com'
  60. }
  61. }
  62. }),
  63. '../../models/Project': {
  64. Project: this.Project
  65. },
  66. '../Project/ProjectEntityHandler': this.ProjectEntityHandler,
  67. '../Project/ProjectGetter': this.ProjectGetter,
  68. '../Project/ProjectRootDocManager': this.ProjectRootDocManager,
  69. '../DocumentUpdater/DocumentUpdaterHandler': this
  70. .DocumentUpdaterHandler,
  71. './ClsiCookieManager': () => this.ClsiCookieManager,
  72. './ClsiStateManager': this.ClsiStateManager,
  73. 'logger-sharelatex': this.logger,
  74. request: this.request,
  75. './ClsiFormatChecker': this.ClsiFormatChecker,
  76. 'metrics-sharelatex': this.Metrics
  77. }
  78. })
  79. this.project_id = 'project-id'
  80. this.user_id = 'user-id'
  81. this.callback = sinon.stub()
  82. })
  83. describe('sendRequest', function() {
  84. beforeEach(function() {
  85. this.ClsiManager._buildRequest = sinon
  86. .stub()
  87. .callsArgWith(2, null, (this.request = 'mock-request'))
  88. this.ClsiCookieManager._getServerId.callsArgWith(1, null, 'clsi3')
  89. })
  90. describe('with a successful compile', function() {
  91. beforeEach(function() {
  92. this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
  93. compile: {
  94. status: (this.status = 'success'),
  95. outputFiles: [
  96. {
  97. url: `${this.settings.apis.clsi.url}/project/${
  98. this.project_id
  99. }/user/${this.user_id}/build/1234/output/output.pdf`,
  100. path: 'output.pdf',
  101. type: 'pdf',
  102. build: 1234
  103. },
  104. {
  105. url: `${this.settings.apis.clsi.url}/project/${
  106. this.project_id
  107. }/user/${this.user_id}/build/1234/output/output.log`,
  108. path: 'output.log',
  109. type: 'log',
  110. build: 1234
  111. }
  112. ]
  113. }
  114. })
  115. this.ClsiManager.sendRequest(
  116. this.project_id,
  117. this.user_id,
  118. { compileGroup: 'standard' },
  119. this.callback
  120. )
  121. })
  122. it('should build the request', function() {
  123. this.ClsiManager._buildRequest
  124. .calledWith(this.project_id)
  125. .should.equal(true)
  126. })
  127. it('should send the request to the CLSI', function() {
  128. this.ClsiManager._postToClsi
  129. .calledWith(this.project_id, this.user_id, this.request, 'standard')
  130. .should.equal(true)
  131. })
  132. it('should call the callback with the status and output files', function() {
  133. const outputFiles = [
  134. {
  135. url: `/project/${this.project_id}/user/${
  136. this.user_id
  137. }/build/1234/output/output.pdf`,
  138. path: 'output.pdf',
  139. type: 'pdf',
  140. build: 1234
  141. },
  142. {
  143. url: `/project/${this.project_id}/user/${
  144. this.user_id
  145. }/build/1234/output/output.log`,
  146. path: 'output.log',
  147. type: 'log',
  148. build: 1234
  149. }
  150. ]
  151. this.callback
  152. .calledWith(null, this.status, outputFiles)
  153. .should.equal(true)
  154. })
  155. })
  156. describe('with a failed compile', function() {
  157. beforeEach(function() {
  158. this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
  159. compile: {
  160. status: (this.status = 'failure')
  161. }
  162. })
  163. this.ClsiManager.sendRequest(
  164. this.project_id,
  165. this.user_id,
  166. {},
  167. this.callback
  168. )
  169. })
  170. it('should call the callback with a failure status', function() {
  171. this.callback.calledWith(null, this.status).should.equal(true)
  172. })
  173. })
  174. describe('with a sync conflict', function() {
  175. beforeEach(function() {
  176. this.ClsiManager.sendRequestOnce = sinon.stub()
  177. this.ClsiManager.sendRequestOnce
  178. .withArgs(this.project_id, this.user_id, { syncType: 'full' })
  179. .callsArgWith(3, null, (this.status = 'success'))
  180. this.ClsiManager.sendRequestOnce
  181. .withArgs(this.project_id, this.user_id, {})
  182. .callsArgWith(3, null, 'conflict')
  183. this.ClsiManager.sendRequest(
  184. this.project_id,
  185. this.user_id,
  186. {},
  187. this.callback
  188. )
  189. })
  190. it('should call the sendRequestOnce method twice', function() {
  191. this.ClsiManager.sendRequestOnce.calledTwice.should.equal(true)
  192. })
  193. it('should call the sendRequestOnce method with syncType:full', function() {
  194. this.ClsiManager.sendRequestOnce
  195. .calledWith(this.project_id, this.user_id, { syncType: 'full' })
  196. .should.equal(true)
  197. })
  198. it('should call the sendRequestOnce method without syncType:full', function() {
  199. this.ClsiManager.sendRequestOnce
  200. .calledWith(this.project_id, this.user_id, {})
  201. .should.equal(true)
  202. })
  203. it('should call the callback with a success status', function() {
  204. this.callback.calledWith(null, this.status).should.equal(true)
  205. })
  206. })
  207. describe('when the resources fail the precompile check', function() {
  208. beforeEach(function() {
  209. this.ClsiFormatChecker.checkRecoursesForProblems = sinon
  210. .stub()
  211. .callsArgWith(1, new Error('failed'))
  212. this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
  213. compile: {
  214. status: (this.status = 'failure')
  215. }
  216. })
  217. this.ClsiManager.sendRequest(
  218. this.project_id,
  219. this.user_id,
  220. {},
  221. this.callback
  222. )
  223. })
  224. it('should call the callback only once', function() {
  225. this.callback.calledOnce.should.equal(true)
  226. })
  227. it('should call the callback with an error', function() {
  228. this.callback.should.have.been.calledWith(sinon.match.instanceOf(Error))
  229. })
  230. })
  231. })
  232. describe('sendExternalRequest', function() {
  233. beforeEach(function() {
  234. this.submission_id = 'submission-id'
  235. this.clsi_request = 'mock-request'
  236. this.ClsiCookieManager._getServerId.callsArgWith(1, null, 'clsi3')
  237. })
  238. describe('with a successful compile', function() {
  239. beforeEach(function() {
  240. this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
  241. compile: {
  242. status: (this.status = 'success'),
  243. outputFiles: [
  244. {
  245. url: `${this.settings.apis.clsi.url}/project/${
  246. this.submission_id
  247. }/build/1234/output/output.pdf`,
  248. path: 'output.pdf',
  249. type: 'pdf',
  250. build: 1234
  251. },
  252. {
  253. url: `${this.settings.apis.clsi.url}/project/${
  254. this.submission_id
  255. }/build/1234/output/output.log`,
  256. path: 'output.log',
  257. type: 'log',
  258. build: 1234
  259. }
  260. ]
  261. }
  262. })
  263. this.ClsiManager.sendExternalRequest(
  264. this.submission_id,
  265. this.clsi_request,
  266. { compileGroup: 'standard' },
  267. this.callback
  268. )
  269. })
  270. it('should send the request to the CLSI', function() {
  271. this.ClsiManager._postToClsi
  272. .calledWith(this.submission_id, null, this.clsi_request, 'standard')
  273. .should.equal(true)
  274. })
  275. it('should call the callback with the status and output files', function() {
  276. const outputFiles = [
  277. {
  278. url: `/project/${this.submission_id}/build/1234/output/output.pdf`,
  279. path: 'output.pdf',
  280. type: 'pdf',
  281. build: 1234
  282. },
  283. {
  284. url: `/project/${this.submission_id}/build/1234/output/output.log`,
  285. path: 'output.log',
  286. type: 'log',
  287. build: 1234
  288. }
  289. ]
  290. this.callback
  291. .calledWith(null, this.status, outputFiles)
  292. .should.equal(true)
  293. })
  294. })
  295. describe('with a failed compile', function() {
  296. beforeEach(function() {
  297. this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
  298. compile: {
  299. status: (this.status = 'failure')
  300. }
  301. })
  302. this.ClsiManager.sendExternalRequest(
  303. this.submission_id,
  304. this.clsi_request,
  305. {},
  306. this.callback
  307. )
  308. })
  309. it('should call the callback with a failure status', function() {
  310. this.callback.calledWith(null, this.status).should.equal(true)
  311. })
  312. })
  313. describe('when the resources fail the precompile check', function() {
  314. beforeEach(function() {
  315. this.ClsiFormatChecker.checkRecoursesForProblems = sinon
  316. .stub()
  317. .callsArgWith(1, new Error('failed'))
  318. this.ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
  319. compile: {
  320. status: (this.status = 'failure')
  321. }
  322. })
  323. this.ClsiManager.sendExternalRequest(
  324. this.submission_id,
  325. this.clsi_request,
  326. {},
  327. this.callback
  328. )
  329. })
  330. it('should call the callback only once', function() {
  331. this.callback.calledOnce.should.equal(true)
  332. })
  333. it('should call the callback with an error', function() {
  334. this.callback.should.have.been.calledWith(sinon.match.instanceOf(Error))
  335. })
  336. })
  337. })
  338. describe('deleteAuxFiles', function() {
  339. beforeEach(function() {
  340. this.ClsiManager._makeRequest = sinon.stub().callsArg(2)
  341. this.DocumentUpdaterHandler.clearProjectState = sinon.stub().callsArg(1)
  342. })
  343. describe('with the standard compileGroup', function() {
  344. beforeEach(function() {
  345. this.ClsiManager.deleteAuxFiles(
  346. this.project_id,
  347. this.user_id,
  348. { compileGroup: 'standard' },
  349. this.callback
  350. )
  351. })
  352. it('should call the delete method in the standard CLSI', function() {
  353. this.ClsiManager._makeRequest
  354. .calledWith(this.project_id, {
  355. method: 'DELETE',
  356. url: `${this.settings.apis.clsi.url}/project/${
  357. this.project_id
  358. }/user/${this.user_id}`
  359. })
  360. .should.equal(true)
  361. })
  362. it('should clear the project state from the docupdater', function() {
  363. this.DocumentUpdaterHandler.clearProjectState
  364. .calledWith(this.project_id)
  365. .should.equal(true)
  366. })
  367. it('should call the callback', function() {
  368. this.callback.called.should.equal(true)
  369. })
  370. })
  371. })
  372. describe('_buildRequest', function() {
  373. beforeEach(function() {
  374. this.project = {
  375. _id: this.project_id,
  376. compiler: (this.compiler = 'latex'),
  377. rootDoc_id: 'mock-doc-id-1',
  378. imageName: (this.image = 'mock-image-name')
  379. }
  380. this.docs = {
  381. '/main.tex': (this.doc_1 = {
  382. name: 'main.tex',
  383. _id: 'mock-doc-id-1',
  384. lines: ['Hello', 'world']
  385. }),
  386. '/chapters/chapter1.tex': (this.doc_2 = {
  387. name: 'chapter1.tex',
  388. _id: 'mock-doc-id-2',
  389. lines: ['Chapter 1']
  390. })
  391. }
  392. this.files = {
  393. '/images/image.png': (this.file_1 = {
  394. name: 'image.png',
  395. _id: 'mock-file-id-1',
  396. created: new Date()
  397. })
  398. }
  399. this.Project.findById = sinon.stub().callsArgWith(2, null, this.project)
  400. this.ProjectEntityHandler.getAllDocs = sinon
  401. .stub()
  402. .callsArgWith(1, null, this.docs)
  403. this.ProjectEntityHandler.getAllFiles = sinon
  404. .stub()
  405. .callsArgWith(1, null, this.files)
  406. this.ProjectGetter.getProject = sinon
  407. .stub()
  408. .callsArgWith(2, null, this.project)
  409. this.DocumentUpdaterHandler.flushProjectToMongo = sinon
  410. .stub()
  411. .callsArgWith(1, null)
  412. })
  413. describe('with a valid project', function() {
  414. beforeEach(function(done) {
  415. this.ClsiManager._buildRequest(
  416. this.project_id,
  417. { timeout: 100 },
  418. (err, request) => {
  419. if (err != null) {
  420. return done(err)
  421. }
  422. this.request = request
  423. done()
  424. }
  425. )
  426. })
  427. it('should get the project with the required fields', function() {
  428. this.ProjectGetter.getProject
  429. .calledWith(this.project_id, {
  430. compiler: 1,
  431. rootDoc_id: 1,
  432. imageName: 1,
  433. rootFolder: 1
  434. })
  435. .should.equal(true)
  436. })
  437. it('should flush the project to the database', function() {
  438. this.DocumentUpdaterHandler.flushProjectToMongo
  439. .calledWith(this.project_id)
  440. .should.equal(true)
  441. })
  442. it('should get all the docs', function() {
  443. this.ProjectEntityHandler.getAllDocs
  444. .calledWith(this.project_id)
  445. .should.equal(true)
  446. })
  447. it('should get all the files', function() {
  448. this.ProjectEntityHandler.getAllFiles
  449. .calledWith(this.project_id)
  450. .should.equal(true)
  451. })
  452. it('should build up the CLSI request', function() {
  453. expect(this.request).to.deep.equal({
  454. compile: {
  455. options: {
  456. compiler: this.compiler,
  457. timeout: 100,
  458. imageName: this.image,
  459. draft: false,
  460. check: undefined,
  461. syncType: undefined, // "full"
  462. syncState: undefined
  463. }, // "01234567890abcdef"
  464. rootResourcePath: 'main.tex',
  465. resources: [
  466. {
  467. path: 'main.tex',
  468. content: this.doc_1.lines.join('\n')
  469. },
  470. {
  471. path: 'chapters/chapter1.tex',
  472. content: this.doc_2.lines.join('\n')
  473. },
  474. {
  475. path: 'images/image.png',
  476. url: `${this.settings.apis.filestore.url}/project/${
  477. this.project_id
  478. }/file/${this.file_1._id}`,
  479. modified: this.file_1.created.getTime()
  480. }
  481. ]
  482. }
  483. })
  484. })
  485. })
  486. describe('with the incremental compile option', function() {
  487. beforeEach(function(done) {
  488. this.ClsiStateManager.computeHash = sinon
  489. .stub()
  490. .callsArgWith(
  491. 2,
  492. null,
  493. (this.project_state_hash = '01234567890abcdef')
  494. )
  495. this.DocumentUpdaterHandler.getProjectDocsIfMatch = sinon
  496. .stub()
  497. .callsArgWith(2, null, [
  498. { _id: this.doc_1._id, lines: this.doc_1.lines, v: 123 }
  499. ])
  500. this.ProjectEntityHandler.getAllDocPathsFromProject = sinon
  501. .stub()
  502. .callsArgWith(1, null, { 'mock-doc-id-1': 'main.tex' })
  503. this.ClsiManager._buildRequest(
  504. this.project_id,
  505. { timeout: 100, incrementalCompilesEnabled: true },
  506. (err, request) => {
  507. if (err != null) {
  508. return done(err)
  509. }
  510. this.request = request
  511. done()
  512. }
  513. )
  514. })
  515. it('should get the project with the required fields', function() {
  516. this.ProjectGetter.getProject
  517. .calledWith(this.project_id, {
  518. compiler: 1,
  519. rootDoc_id: 1,
  520. imageName: 1,
  521. rootFolder: 1
  522. })
  523. .should.equal(true)
  524. })
  525. it('should not explicitly flush the project to the database', function() {
  526. this.DocumentUpdaterHandler.flushProjectToMongo
  527. .calledWith(this.project_id)
  528. .should.equal(false)
  529. })
  530. it('should get only the live docs from the docupdater with a background flush in docupdater', function() {
  531. this.DocumentUpdaterHandler.getProjectDocsIfMatch
  532. .calledWith(this.project_id)
  533. .should.equal(true)
  534. })
  535. it('should not get any of the files', function() {
  536. this.ProjectEntityHandler.getAllFiles.called.should.equal(false)
  537. })
  538. it('should build up the CLSI request', function() {
  539. expect(this.request).to.deep.equal({
  540. compile: {
  541. options: {
  542. compiler: this.compiler,
  543. timeout: 100,
  544. imageName: this.image,
  545. draft: false,
  546. check: undefined,
  547. syncType: 'incremental',
  548. syncState: '01234567890abcdef'
  549. },
  550. rootResourcePath: 'main.tex',
  551. resources: [
  552. {
  553. path: 'main.tex',
  554. content: this.doc_1.lines.join('\n')
  555. }
  556. ]
  557. }
  558. })
  559. })
  560. describe('when the root doc is set and not in the docupdater', function() {
  561. beforeEach(function(done) {
  562. this.ClsiStateManager.computeHash = sinon
  563. .stub()
  564. .callsArgWith(
  565. 2,
  566. null,
  567. (this.project_state_hash = '01234567890abcdef')
  568. )
  569. this.DocumentUpdaterHandler.getProjectDocsIfMatch = sinon
  570. .stub()
  571. .callsArgWith(2, null, [
  572. { _id: this.doc_1._id, lines: this.doc_1.lines, v: 123 }
  573. ])
  574. this.ProjectEntityHandler.getAllDocPathsFromProject = sinon
  575. .stub()
  576. .callsArgWith(1, null, {
  577. 'mock-doc-id-1': 'main.tex',
  578. 'mock-doc-id-2': '/chapters/chapter1.tex'
  579. })
  580. this.ClsiManager._buildRequest(
  581. this.project_id,
  582. {
  583. timeout: 100,
  584. incrementalCompilesEnabled: true,
  585. rootDoc_id: 'mock-doc-id-2'
  586. },
  587. (err, request) => {
  588. if (err != null) {
  589. return done(err)
  590. }
  591. this.request = request
  592. done()
  593. }
  594. )
  595. })
  596. it('should still change the root path', function() {
  597. this.request.compile.rootResourcePath.should.equal(
  598. 'chapters/chapter1.tex'
  599. )
  600. })
  601. })
  602. })
  603. describe('when root doc override is valid', function() {
  604. beforeEach(function(done) {
  605. this.ClsiManager._buildRequest(
  606. this.project_id,
  607. { rootDoc_id: 'mock-doc-id-2' },
  608. (err, request) => {
  609. if (err != null) {
  610. return done(err)
  611. }
  612. this.request = request
  613. done()
  614. }
  615. )
  616. })
  617. it('should change root path', function() {
  618. this.request.compile.rootResourcePath.should.equal(
  619. 'chapters/chapter1.tex'
  620. )
  621. })
  622. })
  623. describe('when root doc override is invalid', function() {
  624. beforeEach(function(done) {
  625. this.ClsiManager._buildRequest(
  626. this.project_id,
  627. { rootDoc_id: 'invalid-id' },
  628. (err, request) => {
  629. if (err != null) {
  630. return done(err)
  631. }
  632. this.request = request
  633. done()
  634. }
  635. )
  636. })
  637. it('should fallback to default root doc', function() {
  638. this.request.compile.rootResourcePath.should.equal('main.tex')
  639. })
  640. })
  641. describe('when the project has an invalid compiler', function() {
  642. beforeEach(function(done) {
  643. this.project.compiler = 'context'
  644. this.ClsiManager._buildRequest(this.project, null, (err, request) => {
  645. if (err != null) {
  646. return done(err)
  647. }
  648. this.request = request
  649. done()
  650. })
  651. })
  652. it('should set the compiler to pdflatex', function() {
  653. this.request.compile.options.compiler.should.equal('pdflatex')
  654. })
  655. })
  656. describe('when there is no valid root document', function() {
  657. beforeEach(function(done) {
  658. this.project.rootDoc_id = 'not-valid'
  659. this.ClsiManager._buildRequest(this.project, null, (error, request) => {
  660. this.error = error
  661. this.request = request
  662. done()
  663. })
  664. })
  665. it('should set to main.tex', function() {
  666. this.request.compile.rootResourcePath.should.equal('main.tex')
  667. })
  668. })
  669. describe('when there is no valid root document and no main.tex document', function() {
  670. beforeEach(function() {
  671. this.project.rootDoc_id = 'not-valid'
  672. this.docs = {
  673. '/other.tex': (this.doc_1 = {
  674. name: 'other.tex',
  675. _id: 'mock-doc-id-1',
  676. lines: ['Hello', 'world']
  677. }),
  678. '/chapters/chapter1.tex': (this.doc_2 = {
  679. name: 'chapter1.tex',
  680. _id: 'mock-doc-id-2',
  681. lines: ['Chapter 1']
  682. })
  683. }
  684. this.ProjectEntityHandler.getAllDocs = sinon
  685. .stub()
  686. .callsArgWith(1, null, this.docs)
  687. this.ClsiManager._buildRequest(this.project, null, this.callback)
  688. })
  689. it('should report an error', function() {
  690. this.callback.should.have.been.calledWith(sinon.match.instanceOf(Error))
  691. })
  692. })
  693. describe('when there is no valid root document and a single document which is not main.tex', function() {
  694. beforeEach(function(done) {
  695. this.project.rootDoc_id = 'not-valid'
  696. this.docs = {
  697. '/other.tex': (this.doc_1 = {
  698. name: 'other.tex',
  699. _id: 'mock-doc-id-1',
  700. lines: ['Hello', 'world']
  701. })
  702. }
  703. this.ProjectEntityHandler.getAllDocs = sinon
  704. .stub()
  705. .callsArgWith(1, null, this.docs)
  706. this.ClsiManager._buildRequest(this.project, null, (error, request) => {
  707. this.error = error
  708. this.request = request
  709. done()
  710. })
  711. })
  712. it('should set io to the only file', function() {
  713. this.request.compile.rootResourcePath.should.equal('other.tex')
  714. })
  715. })
  716. describe('with the draft option', function() {
  717. it('should add the draft option into the request', function(done) {
  718. this.ClsiManager._buildRequest(
  719. this.project_id,
  720. { timeout: 100, draft: true },
  721. (err, request) => {
  722. if (err != null) {
  723. return done(err)
  724. }
  725. request.compile.options.draft.should.equal(true)
  726. done()
  727. }
  728. )
  729. })
  730. })
  731. })
  732. describe('_postToClsi', function() {
  733. beforeEach(function() {
  734. this.req = { mock: 'req', compile: {} }
  735. })
  736. describe('successfully', function() {
  737. beforeEach(function() {
  738. this.ClsiManager._makeRequest = sinon
  739. .stub()
  740. .callsArgWith(
  741. 2,
  742. null,
  743. { statusCode: 204 },
  744. (this.body = { mock: 'foo' })
  745. )
  746. this.ClsiManager._postToClsi(
  747. this.project_id,
  748. this.user_id,
  749. this.req,
  750. 'standard',
  751. this.callback
  752. )
  753. })
  754. it('should send the request to the CLSI', function() {
  755. const url = `${this.settings.apis.clsi.url}/project/${
  756. this.project_id
  757. }/user/${this.user_id}/compile`
  758. this.ClsiManager._makeRequest
  759. .calledWith(this.project_id, {
  760. method: 'POST',
  761. url,
  762. json: this.req
  763. })
  764. .should.equal(true)
  765. })
  766. it('should call the callback with the body and no error', function() {
  767. this.callback.calledWith(null, this.body).should.equal(true)
  768. })
  769. })
  770. describe('when the CLSI returns an error', function() {
  771. beforeEach(function() {
  772. this.ClsiManager._makeRequest = sinon
  773. .stub()
  774. .callsArgWith(
  775. 2,
  776. null,
  777. { statusCode: 500 },
  778. (this.body = { mock: 'foo' })
  779. )
  780. this.ClsiManager._postToClsi(
  781. this.project_id,
  782. this.user_id,
  783. this.req,
  784. 'standard',
  785. this.callback
  786. )
  787. })
  788. it('should call the callback with an error', function() {
  789. this.callback.should.have.been.calledWith(sinon.match.instanceOf(Error))
  790. })
  791. })
  792. })
  793. describe('wordCount', function() {
  794. beforeEach(function() {
  795. this.ClsiManager._makeRequest = sinon
  796. .stub()
  797. .callsArgWith(
  798. 2,
  799. null,
  800. { statusCode: 200 },
  801. (this.body = { mock: 'foo' })
  802. )
  803. this.ClsiManager._buildRequest = sinon.stub().callsArgWith(
  804. 2,
  805. null,
  806. (this.req = {
  807. compile: { rootResourcePath: 'rootfile.text', options: {} }
  808. })
  809. )
  810. })
  811. describe('with root file', function() {
  812. beforeEach(function() {
  813. this.ClsiManager.wordCount(
  814. this.project_id,
  815. this.user_id,
  816. false,
  817. {},
  818. this.callback
  819. )
  820. })
  821. it('should call wordCount with root file', function() {
  822. this.ClsiManager._makeRequest
  823. .calledWith(this.project_id, {
  824. method: 'GET',
  825. url: `http://clsi.example.com/project/${this.project_id}/user/${
  826. this.user_id
  827. }/wordcount`,
  828. qs: { file: 'rootfile.text', image: undefined }
  829. })
  830. .should.equal(true)
  831. })
  832. it('should call the callback', function() {
  833. this.callback.called.should.equal(true)
  834. })
  835. })
  836. describe('with param file', function() {
  837. beforeEach(function() {
  838. this.ClsiManager.wordCount(
  839. this.project_id,
  840. this.user_id,
  841. 'main.tex',
  842. {},
  843. this.callback
  844. )
  845. })
  846. it('should call wordCount with param file', function() {
  847. this.ClsiManager._makeRequest
  848. .calledWith(this.project_id, {
  849. method: 'GET',
  850. url: `http://clsi.example.com/project/${this.project_id}/user/${
  851. this.user_id
  852. }/wordcount`,
  853. qs: { file: 'main.tex', image: undefined }
  854. })
  855. .should.equal(true)
  856. })
  857. })
  858. describe('with image', function() {
  859. beforeEach(function() {
  860. this.req.compile.options.imageName = this.image =
  861. 'example.com/mock/image'
  862. this.ClsiManager.wordCount(
  863. this.project_id,
  864. this.user_id,
  865. 'main.tex',
  866. {},
  867. this.callback
  868. )
  869. })
  870. it('should call wordCount with file and image', function() {
  871. this.ClsiManager._makeRequest
  872. .calledWith(this.project_id, {
  873. method: 'GET',
  874. url: `http://clsi.example.com/project/${this.project_id}/user/${
  875. this.user_id
  876. }/wordcount`,
  877. qs: { file: 'main.tex', image: this.image }
  878. })
  879. .should.equal(true)
  880. })
  881. })
  882. })
  883. describe('_makeRequest', function() {
  884. beforeEach(function() {
  885. this.response = { there: 'something' }
  886. this.request.callsArgWith(1, null, this.response)
  887. this.opts = {
  888. method: 'SOMETHIGN',
  889. url: 'http://a place on the web'
  890. }
  891. })
  892. it('should process a request with a cookie jar', function(done) {
  893. this.ClsiManager._makeRequest(this.project_id, this.opts, () => {
  894. const args = this.request.args[0]
  895. args[0].method.should.equal(this.opts.method)
  896. args[0].url.should.equal(this.opts.url)
  897. args[0].jar.should.equal(this.jar)
  898. done()
  899. })
  900. })
  901. it('should set the cookie again on response as it might have changed', function(done) {
  902. this.ClsiManager._makeRequest(this.project_id, this.opts, () => {
  903. this.ClsiCookieManager.setServerId
  904. .calledWith(this.project_id, this.response)
  905. .should.equal(true)
  906. done()
  907. })
  908. })
  909. })
  910. describe('_makeGoogleCloudRequest', function() {
  911. beforeEach(function() {
  912. this.settings.apis.clsi_new = { url: 'https://compiles.somewhere.test' }
  913. this.response = { there: 'something' }
  914. this.request.callsArgWith(1, null, this.response)
  915. this.opts = {
  916. url: this.ClsiManager._getCompilerUrl(null, this.project_id)
  917. }
  918. })
  919. it('should change the domain on the url', function(done) {
  920. this.ClsiManager._makeNewBackendRequest(
  921. this.project_id,
  922. this.opts,
  923. () => {
  924. const args = this.request.args[0]
  925. args[0].url.should.equal(
  926. `https://compiles.somewhere.test/project/${this.project_id}`
  927. )
  928. done()
  929. }
  930. )
  931. })
  932. it('should not make a request if there is not clsi_new url', function(done) {
  933. this.settings.apis.clsi_new = undefined
  934. this.ClsiManager._makeNewBackendRequest(
  935. this.project_id,
  936. this.opts,
  937. err => {
  938. expect(err).to.equal(undefined)
  939. this.request.callCount.should.equal(0)
  940. done()
  941. }
  942. )
  943. })
  944. })
  945. })