ClsiManagerTests.js 31 KB

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