ResourceWriterTests.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. SandboxedModule = require('sandboxed-module')
  2. sinon = require('sinon')
  3. should = require('chai').should()
  4. modulePath = require('path').join __dirname, '../../../app/js/ResourceWriter'
  5. path = require "path"
  6. describe "ResourceWriter", ->
  7. beforeEach ->
  8. @ResourceWriter = SandboxedModule.require modulePath, requires:
  9. "fs": @fs =
  10. mkdir: sinon.stub().callsArg(1)
  11. unlink: sinon.stub().callsArg(1)
  12. "./ResourceStateManager": @ResourceStateManager = {}
  13. "wrench": @wrench = {}
  14. "./UrlCache" : @UrlCache = {}
  15. "mkdirp" : @mkdirp = sinon.stub().callsArg(1)
  16. "./OutputFileFinder": @OutputFileFinder = {}
  17. "logger-sharelatex": {log: sinon.stub(), err: sinon.stub()}
  18. "./Metrics": @Metrics =
  19. Timer: class Timer
  20. done: sinon.stub()
  21. @project_id = "project-id-123"
  22. @basePath = "/path/to/write/files/to"
  23. @callback = sinon.stub()
  24. describe "syncResourcesToDisk on a full request", ->
  25. beforeEach ->
  26. @resources = [
  27. "resource-1-mock"
  28. "resource-2-mock"
  29. "resource-3-mock"
  30. ]
  31. @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
  32. @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArg(2)
  33. @ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
  34. @ResourceWriter.syncResourcesToDisk({
  35. project_id: @project_id
  36. syncState: @syncState = "0123456789abcdef"
  37. resources: @resources
  38. }, @basePath, @callback)
  39. it "should remove old files", ->
  40. @ResourceWriter._removeExtraneousFiles
  41. .calledWith(@resources, @basePath)
  42. .should.equal true
  43. it "should write each resource to disk", ->
  44. for resource in @resources
  45. @ResourceWriter._writeResourceToDisk
  46. .calledWith(@project_id, resource, @basePath)
  47. .should.equal true
  48. it "should store the sync state and resource list", ->
  49. @ResourceStateManager.saveProjectState
  50. .calledWith(@syncState, @resources, @basePath)
  51. .should.equal true
  52. it "should call the callback", ->
  53. @callback.called.should.equal true
  54. describe "syncResourcesToDisk on an incremental update", ->
  55. beforeEach ->
  56. @resources = [
  57. "resource-1-mock"
  58. ]
  59. @ResourceWriter._writeResourceToDisk = sinon.stub().callsArg(3)
  60. @ResourceWriter._removeExtraneousFiles = sinon.stub().callsArgWith(2, null, @outputFiles = [], @allFiles = [])
  61. @ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, null, @resources)
  62. @ResourceStateManager.saveProjectState = sinon.stub().callsArg(3)
  63. @ResourceStateManager.checkResourceFiles = sinon.stub().callsArg(3)
  64. @ResourceWriter.syncResourcesToDisk({
  65. project_id: @project_id,
  66. syncType: "incremental",
  67. syncState: @syncState = "1234567890abcdef",
  68. resources: @resources
  69. }, @basePath, @callback)
  70. it "should check the sync state matches", ->
  71. @ResourceStateManager.checkProjectStateMatches
  72. .calledWith(@syncState, @basePath)
  73. .should.equal true
  74. it "should remove old files", ->
  75. @ResourceWriter._removeExtraneousFiles
  76. .calledWith(@resources, @basePath)
  77. .should.equal true
  78. it "should check each resource exists", ->
  79. @ResourceStateManager.checkResourceFiles
  80. .calledWith(@resources, @allFiles, @basePath)
  81. .should.equal true
  82. it "should write each resource to disk", ->
  83. for resource in @resources
  84. @ResourceWriter._writeResourceToDisk
  85. .calledWith(@project_id, resource, @basePath)
  86. .should.equal true
  87. it "should call the callback", ->
  88. @callback.called.should.equal true
  89. describe "syncResourcesToDisk on an incremental update when the state does not match", ->
  90. beforeEach ->
  91. @resources = [
  92. "resource-1-mock"
  93. ]
  94. @ResourceStateManager.checkProjectStateMatches = sinon.stub().callsArgWith(2, @error = new Error())
  95. @ResourceWriter.syncResourcesToDisk({
  96. project_id: @project_id,
  97. syncType: "incremental",
  98. syncState: @syncState = "1234567890abcdef",
  99. resources: @resources
  100. }, @basePath, @callback)
  101. it "should check whether the sync state matches", ->
  102. @ResourceStateManager.checkProjectStateMatches
  103. .calledWith(@syncState, @basePath)
  104. .should.equal true
  105. it "should call the callback with an error", ->
  106. @callback.calledWith(@error).should.equal true
  107. describe "_removeExtraneousFiles", ->
  108. beforeEach ->
  109. @output_files = [{
  110. path: "output.pdf"
  111. type: "pdf"
  112. }, {
  113. path: "extra/file.tex"
  114. type: "tex"
  115. }, {
  116. path: "extra.aux"
  117. type: "aux"
  118. }, {
  119. path: "cache/_chunk1"
  120. },{
  121. path: "figures/image-eps-converted-to.pdf"
  122. type: "pdf"
  123. },{
  124. path: "foo/main-figure0.md5"
  125. type: "md5"
  126. }, {
  127. path: "foo/main-figure0.dpth"
  128. type: "dpth"
  129. }, {
  130. path: "foo/main-figure0.pdf"
  131. type: "pdf"
  132. }, {
  133. path: "_minted-main/default-pyg-prefix.pygstyle"
  134. type: "pygstyle"
  135. }, {
  136. path: "_minted-main/default.pygstyle"
  137. type: "pygstyle"
  138. }, {
  139. path: "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex"
  140. type: "pygtex"
  141. }, {
  142. path: "_markdown_main/30893013dec5d869a415610079774c2f.md.tex"
  143. type: "tex"
  144. }]
  145. @resources = "mock-resources"
  146. @OutputFileFinder.findOutputFiles = sinon.stub().callsArgWith(2, null, @output_files)
  147. @ResourceWriter._deleteFileIfNotDirectory = sinon.stub().callsArg(1)
  148. @ResourceWriter._removeExtraneousFiles(@resources, @basePath, @callback)
  149. it "should find the existing output files", ->
  150. @OutputFileFinder.findOutputFiles
  151. .calledWith(@resources, @basePath)
  152. .should.equal true
  153. it "should delete the output files", ->
  154. @ResourceWriter._deleteFileIfNotDirectory
  155. .calledWith(path.join(@basePath, "output.pdf"))
  156. .should.equal true
  157. it "should delete the extra files", ->
  158. @ResourceWriter._deleteFileIfNotDirectory
  159. .calledWith(path.join(@basePath, "extra/file.tex"))
  160. .should.equal true
  161. it "should not delete the extra aux files", ->
  162. @ResourceWriter._deleteFileIfNotDirectory
  163. .calledWith(path.join(@basePath, "extra.aux"))
  164. .should.equal false
  165. it "should not delete the knitr cache file", ->
  166. @ResourceWriter._deleteFileIfNotDirectory
  167. .calledWith(path.join(@basePath, "cache/_chunk1"))
  168. .should.equal false
  169. it "should not delete the epstopdf converted files", ->
  170. @ResourceWriter._deleteFileIfNotDirectory
  171. .calledWith(path.join(@basePath, "figures/image-eps-converted-to.pdf"))
  172. .should.equal false
  173. it "should not delete the tikz md5 files", ->
  174. @ResourceWriter._deleteFileIfNotDirectory
  175. .calledWith(path.join(@basePath, "foo/main-figure0.md5"))
  176. .should.equal false
  177. it "should not delete the tikz dpth files", ->
  178. @ResourceWriter._deleteFileIfNotDirectory
  179. .calledWith(path.join(@basePath, "foo/main-figure0.dpth"))
  180. .should.equal false
  181. it "should not delete the tikz pdf files", ->
  182. @ResourceWriter._deleteFileIfNotDirectory
  183. .calledWith(path.join(@basePath, "foo/main-figure0.pdf"))
  184. .should.equal false
  185. it "should not delete the minted pygstyle files", ->
  186. @ResourceWriter._deleteFileIfNotDirectory
  187. .calledWith(path.join(@basePath, "_minted-main/default-pyg-prefix.pygstyle"))
  188. .should.equal false
  189. it "should not delete the minted default pygstyle files", ->
  190. @ResourceWriter._deleteFileIfNotDirectory
  191. .calledWith(path.join(@basePath, "_minted-main/default.pygstyle"))
  192. .should.equal false
  193. it "should not delete the minted default pygtex files", ->
  194. @ResourceWriter._deleteFileIfNotDirectory
  195. .calledWith(path.join(@basePath, "_minted-main/35E248B60965545BD232AE9F0FE9750D504A7AF0CD3BAA7542030FC560DFCC45.pygtex"))
  196. .should.equal false
  197. it "should not delete the markdown md.tex files", ->
  198. @ResourceWriter._deleteFileIfNotDirectory
  199. .calledWith(path.join(@basePath, "_markdown_main/30893013dec5d869a415610079774c2f.md.tex"))
  200. .should.equal false
  201. it "should call the callback", ->
  202. @callback.called.should.equal true
  203. it "should time the request", ->
  204. @Metrics.Timer::done.called.should.equal true
  205. describe "_writeResourceToDisk", ->
  206. describe "with a url based resource", ->
  207. beforeEach ->
  208. @resource =
  209. path: "main.tex"
  210. url: "http://www.example.com/main.tex"
  211. modified: Date.now()
  212. @UrlCache.downloadUrlToFile = sinon.stub().callsArgWith(4, "fake error downloading file")
  213. @ResourceWriter._writeResourceToDisk(@project_id, @resource, @basePath, @callback)
  214. it "should ensure the directory exists", ->
  215. @mkdirp
  216. .calledWith(path.dirname(path.join(@basePath, @resource.path)))
  217. .should.equal true
  218. it "should write the URL from the cache", ->
  219. @UrlCache.downloadUrlToFile
  220. .calledWith(@project_id, @resource.url, path.join(@basePath, @resource.path), @resource.modified)
  221. .should.equal true
  222. it "should call the callback", ->
  223. @callback.called.should.equal true
  224. it "should not return an error if the resource writer errored", ->
  225. should.not.exist @callback.args[0][0]
  226. describe "with a content based resource", ->
  227. beforeEach ->
  228. @resource =
  229. path: "main.tex"
  230. content: "Hello world"
  231. @fs.writeFile = sinon.stub().callsArg(2)
  232. @ResourceWriter._writeResourceToDisk(@project_id, @resource, @basePath, @callback)
  233. it "should ensure the directory exists", ->
  234. @mkdirp
  235. .calledWith(path.dirname(path.join(@basePath, @resource.path)))
  236. .should.equal true
  237. it "should write the contents to disk", ->
  238. @fs.writeFile
  239. .calledWith(path.join(@basePath, @resource.path), @resource.content)
  240. .should.equal true
  241. it "should call the callback", ->
  242. @callback.called.should.equal true
  243. describe "with a file path that breaks out of the root folder", ->
  244. beforeEach ->
  245. @resource =
  246. path: "../../main.tex"
  247. content: "Hello world"
  248. @fs.writeFile = sinon.stub().callsArg(2)
  249. @ResourceWriter._writeResourceToDisk(@project_id, @resource, @basePath, @callback)
  250. it "should not write to disk", ->
  251. @fs.writeFile.called.should.equal false
  252. it "should return an error", ->
  253. @callback
  254. .calledWith(new Error("resource path is outside root directory"))
  255. .should.equal true
  256. describe "checkPath", ->
  257. describe "with a valid path", ->
  258. beforeEach ->
  259. @ResourceWriter.checkPath("foo", "bar", @callback)
  260. it "should return the joined path", ->
  261. @callback.calledWith(null, "foo/bar")
  262. .should.equal true
  263. describe "with an invalid path", ->
  264. beforeEach ->
  265. @ResourceWriter.checkPath("foo", "baz/../../bar", @callback)
  266. it "should return an error", ->
  267. @callback.calledWith(new Error("resource path is outside root directory"))
  268. .should.equal true
  269. describe "with another invalid path matching on a prefix", ->
  270. beforeEach ->
  271. @ResourceWriter.checkPath("foo", "../foobar/baz", @callback)
  272. it "should return an error", ->
  273. @callback.calledWith(new Error("resource path is outside root directory"))
  274. .should.equal true