ClsiManagerTests.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. sinon = require('sinon')
  2. chai = require('chai')
  3. should = chai.should()
  4. expect = chai.expect
  5. modulePath = "../../../../app/js/Features/Compile/ClsiManager.js"
  6. SandboxedModule = require('sandboxed-module')
  7. describe "ClsiManager", ->
  8. beforeEach ->
  9. @jar = {cookie:"stuff"}
  10. @ClsiCookieManager =
  11. getCookieJar: sinon.stub().callsArgWith(1, null, @jar)
  12. setServerId: sinon.stub().callsArgWith(2)
  13. _getServerId:sinon.stub()
  14. @ClsiStateManager =
  15. computeHash: sinon.stub().callsArgWith(1, null, "01234567890abcdef")
  16. @ClsiFormatChecker =
  17. checkRecoursesForProblems:sinon.stub().callsArgWith(1)
  18. @ClsiManager = SandboxedModule.require modulePath, requires:
  19. "settings-sharelatex": @settings =
  20. apis:
  21. filestore:
  22. url: "filestore.example.com"
  23. secret: "secret"
  24. clsi:
  25. url: "http://clsi.example.com"
  26. clsi_priority:
  27. url: "https://clsipremium.example.com"
  28. "../../models/Project": Project: @Project = {}
  29. "../Project/ProjectEntityHandler": @ProjectEntityHandler = {}
  30. "../Project/ProjectGetter": @ProjectGetter = {}
  31. "../DocumentUpdater/DocumentUpdaterHandler": @DocumentUpdaterHandler =
  32. getProjectDocsIfMatch: sinon.stub().callsArgWith(2,null,null)
  33. "./ClsiCookieManager": @ClsiCookieManager
  34. "./ClsiStateManager": @ClsiStateManager
  35. "logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }
  36. "request": @request = sinon.stub()
  37. "./ClsiFormatChecker": @ClsiFormatChecker
  38. @project_id = "project-id"
  39. @user_id = "user-id"
  40. @callback = sinon.stub()
  41. describe "sendRequest", ->
  42. beforeEach ->
  43. @ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @request = "mock-request")
  44. @ClsiCookieManager._getServerId.callsArgWith(1, null, "clsi3")
  45. describe "with a successful compile", ->
  46. beforeEach ->
  47. @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
  48. compile:
  49. status: @status = "success"
  50. outputFiles: [{
  51. url: "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.pdf"
  52. path: "output.pdf"
  53. type: "pdf"
  54. build: 1234
  55. },{
  56. url: "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.log"
  57. path: "output.log"
  58. type: "log"
  59. build: 1234
  60. }]
  61. })
  62. @ClsiManager.sendRequest @project_id, @user_id, {compileGroup:"standard"}, @callback
  63. it "should build the request", ->
  64. @ClsiManager._buildRequest
  65. .calledWith(@project_id)
  66. .should.equal true
  67. it "should send the request to the CLSI", ->
  68. @ClsiManager._postToClsi
  69. .calledWith(@project_id, @user_id, @request, "standard")
  70. .should.equal true
  71. it "should call the callback with the status and output files", ->
  72. outputFiles = [{
  73. url: "/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.pdf"
  74. path: "output.pdf"
  75. type: "pdf"
  76. build: 1234
  77. },{
  78. url: "/project/#{@project_id}/user/#{@user_id}/build/1234/output/output.log"
  79. path: "output.log"
  80. type: "log"
  81. build: 1234
  82. }]
  83. @callback.calledWith(null, @status, outputFiles).should.equal true
  84. describe "with a failed compile", ->
  85. beforeEach ->
  86. @ClsiManager._postToClsi = sinon.stub().callsArgWith(4, null, {
  87. compile:
  88. status: @status = "failure"
  89. })
  90. @ClsiManager.sendRequest @project_id, @user_id, {}, @callback
  91. it "should call the callback with a failure statue", ->
  92. @callback.calledWith(null, @status).should.equal true
  93. describe "deleteAuxFiles", ->
  94. beforeEach ->
  95. @ClsiManager._makeRequest = sinon.stub().callsArg(2)
  96. describe "with the standard compileGroup", ->
  97. beforeEach ->
  98. @ClsiManager.deleteAuxFiles @project_id, @user_id, {compileGroup: "standard"}, @callback
  99. it "should call the delete method in the standard CLSI", ->
  100. @ClsiManager._makeRequest
  101. .calledWith(@project_id, { method:"DELETE", url:"#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}"})
  102. .should.equal true
  103. it "should call the callback", ->
  104. @callback.called.should.equal true
  105. describe "_buildRequest", ->
  106. beforeEach ->
  107. @project =
  108. _id: @project_id
  109. compiler: @compiler = "latex"
  110. rootDoc_id: "mock-doc-id-1"
  111. imageName: @image = "mock-image-name"
  112. @docs = {
  113. "/main.tex": @doc_1 = {
  114. name: "main.tex"
  115. _id: "mock-doc-id-1"
  116. lines: ["Hello", "world"]
  117. },
  118. "/chapters/chapter1.tex": @doc_2 = {
  119. name: "chapter1.tex"
  120. _id: "mock-doc-id-2"
  121. lines: [
  122. "Chapter 1"
  123. ]
  124. }
  125. }
  126. @files = {
  127. "/images/image.png": @file_1 = {
  128. name: "image.png"
  129. _id: "mock-file-id-1"
  130. created: new Date()
  131. }
  132. }
  133. @Project.findById = sinon.stub().callsArgWith(2, null, @project)
  134. @ProjectEntityHandler.getAllDocs = sinon.stub().callsArgWith(1, null, @docs)
  135. @ProjectEntityHandler.getAllFiles = sinon.stub().callsArgWith(1, null, @files)
  136. @ProjectGetter.getProject = sinon.stub().callsArgWith(2, null, @project)
  137. @DocumentUpdaterHandler.flushProjectToMongo = sinon.stub().callsArgWith(1, null)
  138. describe "with a valid project", ->
  139. beforeEach (done) ->
  140. @ClsiManager._buildRequest @project_id, {timeout:100}, (error, request) =>
  141. @request = request
  142. done()
  143. it "should get the project with the required fields", ->
  144. @ProjectGetter.getProject
  145. .calledWith(@project_id, {compiler:1, rootDoc_id: 1, imageName: 1, rootFolder: 1})
  146. .should.equal true
  147. it "should flush the project to the database", ->
  148. @DocumentUpdaterHandler.flushProjectToMongo
  149. .calledWith(@project_id)
  150. .should.equal true
  151. it "should get all the docs", ->
  152. @ProjectEntityHandler.getAllDocs
  153. .calledWith(@project_id)
  154. .should.equal true
  155. it "should get all the files", ->
  156. @ProjectEntityHandler.getAllFiles
  157. .calledWith(@project_id)
  158. .should.equal true
  159. it "should build up the CLSI request", ->
  160. expect(@request).to.deep.equal(
  161. compile:
  162. options:
  163. compiler: @compiler
  164. timeout : 100
  165. imageName: @image
  166. draft: false
  167. check: undefined
  168. syncType: "full"
  169. syncState: "01234567890abcdef"
  170. rootResourcePath: "main.tex"
  171. resources: [{
  172. path: "main.tex"
  173. content: @doc_1.lines.join("\n")
  174. }, {
  175. path: "chapters/chapter1.tex"
  176. content: @doc_2.lines.join("\n")
  177. }, {
  178. path: "images/image.png"
  179. url: "#{@settings.apis.filestore.url}/project/#{@project_id}/file/#{@file_1._id}"
  180. modified: @file_1.created.getTime()
  181. }]
  182. )
  183. describe "when root doc override is valid", ->
  184. beforeEach (done) ->
  185. @ClsiManager._buildRequest @project_id, {rootDoc_id:"mock-doc-id-2"}, (error, request) =>
  186. @request = request
  187. done()
  188. it "should change root path", ->
  189. @request.compile.rootResourcePath.should.equal "chapters/chapter1.tex"
  190. describe "when root doc override is invalid", ->
  191. beforeEach (done) ->
  192. @ClsiManager._buildRequest @project_id, {rootDoc_id:"invalid-id"}, (error, request) =>
  193. @request = request
  194. done()
  195. it "should fallback to default root doc", ->
  196. @request.compile.rootResourcePath.should.equal "main.tex"
  197. describe "when the project has an invalid compiler", ->
  198. beforeEach (done) ->
  199. @project.compiler = "context"
  200. @ClsiManager._buildRequest @project, null, (error, request) =>
  201. @request = request
  202. done()
  203. it "should set the compiler to pdflatex", ->
  204. @request.compile.options.compiler.should.equal "pdflatex"
  205. describe "when there is no valid root document", ->
  206. beforeEach (done) ->
  207. @project.rootDoc_id = "not-valid"
  208. @ClsiManager._buildRequest @project, null, (@error, @request) =>
  209. done()
  210. it "should set to main.tex", ->
  211. @request.compile.rootResourcePath.should.equal "main.tex"
  212. describe "with the draft option", ->
  213. it "should add the draft option into the request", (done) ->
  214. @ClsiManager._buildRequest @project_id, {timeout:100, draft: true}, (error, request) =>
  215. request.compile.options.draft.should.equal true
  216. done()
  217. describe '_postToClsi', ->
  218. beforeEach ->
  219. @req = { mock: "req" }
  220. describe "successfully", ->
  221. beforeEach ->
  222. @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 204}, @body = { mock: "foo" })
  223. @ClsiManager._postToClsi @project_id, @user_id, @req, "standard", @callback
  224. it 'should send the request to the CLSI', ->
  225. url = "#{@settings.apis.clsi.url}/project/#{@project_id}/user/#{@user_id}/compile"
  226. @ClsiManager._makeRequest.calledWith(@project_id, {
  227. method: "POST",
  228. url: url
  229. json: @req
  230. }).should.equal true
  231. it "should call the callback with the body and no error", ->
  232. @callback.calledWith(null, @body).should.equal true
  233. describe "when the CLSI returns an error", ->
  234. beforeEach ->
  235. @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 500}, @body = { mock: "foo" })
  236. @ClsiManager._postToClsi @project_id, @user_id, @req, "standard", @callback
  237. it "should call the callback with the body and the error", ->
  238. @callback.calledWith(new Error("CLSI returned non-success code: 500"), @body).should.equal true
  239. describe "wordCount", ->
  240. beforeEach ->
  241. @ClsiManager._makeRequest = sinon.stub().callsArgWith(2, null, {statusCode: 200}, @body = { mock: "foo" })
  242. @ClsiManager._buildRequest = sinon.stub().callsArgWith(2, null, @req = { compile: { rootResourcePath: "rootfile.text", options: {} } })
  243. describe "with root file", ->
  244. beforeEach ->
  245. @ClsiManager.wordCount @project_id, @user_id, false, {}, @callback
  246. it "should call wordCount with root file", ->
  247. @ClsiManager._makeRequest
  248. .calledWith(@project_id, {method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs: {file: "rootfile.text",image:undefined}})
  249. .should.equal true
  250. it "should call the callback", ->
  251. @callback.called.should.equal true
  252. describe "with param file", ->
  253. beforeEach ->
  254. @ClsiManager.wordCount @project_id, @user_id, "main.tex", {}, @callback
  255. it "should call wordCount with param file", ->
  256. @ClsiManager._makeRequest
  257. .calledWith(@project_id, { method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs:{file:"main.tex",image:undefined}})
  258. .should.equal true
  259. describe "with image", ->
  260. beforeEach ->
  261. @req.compile.options.imageName = @image = "example.com/mock/image"
  262. @ClsiManager.wordCount @project_id, @user_id, "main.tex", {}, @callback
  263. it "should call wordCount with file and image", ->
  264. @ClsiManager._makeRequest
  265. .calledWith(@project_id, { method: "GET", url: "http://clsi.example.com/project/#{@project_id}/user/#{@user_id}/wordcount", qs:{file:"main.tex",image:@image}})
  266. .should.equal true
  267. describe "_makeRequest", ->
  268. beforeEach ->
  269. @response = {there:"something"}
  270. @request.callsArgWith(1, null, @response)
  271. @opts =
  272. method: "SOMETHIGN"
  273. url: "http://a place on the web"
  274. it "should process a request with a cookie jar", (done)->
  275. @ClsiManager._makeRequest @project_id, @opts, =>
  276. args = @request.args[0]
  277. args[0].method.should.equal @opts.method
  278. args[0].url.should.equal @opts.url
  279. args[0].jar.should.equal @jar
  280. done()
  281. it "should set the cookie again on response as it might have changed", (done)->
  282. @ClsiManager._makeRequest @project_id, @opts, =>
  283. @ClsiCookieManager.setServerId.calledWith(@project_id, @response).should.equal true
  284. done()