DocArchiveManagerTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. assert = require("chai").assert
  2. sinon = require('sinon')
  3. chai = require('chai')
  4. should = chai.should()
  5. expect = chai.expect
  6. modulePath = "../../../app/js/DocArchiveManager.js"
  7. SandboxedModule = require('sandboxed-module')
  8. ObjectId = require("mongojs").ObjectId
  9. Errors = require "../../../app/js/Errors"
  10. crypto = require("crypto")
  11. describe "DocArchiveManager", ->
  12. beforeEach ->
  13. @settings =
  14. docstore:
  15. s3:
  16. secret: "secret"
  17. key: "this_key"
  18. bucket:"doc-archive-unit-test"
  19. @request =
  20. put: {}
  21. get: {}
  22. del: {}
  23. @archivedDocs = [{
  24. _id: ObjectId()
  25. inS3:true
  26. rev: 2
  27. }, {
  28. _id: ObjectId()
  29. inS3:true
  30. rev: 4
  31. }, {
  32. _id: ObjectId()
  33. inS3:true
  34. rev: 6
  35. }]
  36. @mongoDocs = [{
  37. _id: ObjectId()
  38. lines: ["one", "two", "three"]
  39. rev: 2
  40. }, {
  41. _id: ObjectId()
  42. lines: ["aaa", "bbb", "ccc"]
  43. rev: 4
  44. }, {
  45. _id: ObjectId()
  46. inS3: true
  47. rev: 6
  48. }, {
  49. _id: ObjectId()
  50. inS3: true
  51. rev: 6
  52. }, {
  53. _id: ObjectId()
  54. lines: ["111", "222", "333"]
  55. rev: 6
  56. }]
  57. @unarchivedDocs = [{
  58. _id: ObjectId()
  59. lines: ["wombat", "potato", "banana"]
  60. rev: 2
  61. }, {
  62. _id: ObjectId()
  63. lines: ["llama", "turnip", "apple"]
  64. rev: 4
  65. }, {
  66. _id: ObjectId()
  67. lines: ["elephant", "swede", "nectarine"]
  68. rev: 6
  69. }]
  70. @mixedDocs = @archivedDocs.concat(@unarchivedDocs)
  71. @MongoManager =
  72. markDocAsArchived: sinon.stub().callsArgWith(2, null)
  73. upsertIntoDocCollection: sinon.stub().callsArgWith(3, null)
  74. getProjectsDocs: sinon.stub().callsArgWith(3, null, @mongoDocs)
  75. getArchivedProjectDocs: sinon.stub().callsArgWith(2, null, @mongoDocs)
  76. @requires =
  77. "settings-sharelatex": @settings
  78. "./MongoManager": @MongoManager
  79. "request": @request
  80. "./RangeManager": @RangeManager = {}
  81. "logger-sharelatex":
  82. log:->
  83. err:->
  84. @globals =
  85. JSON: JSON
  86. @error = "my errror"
  87. @project_id = ObjectId().toString()
  88. @stubbedError = new Errors.NotFoundError("Error in S3 request")
  89. @DocArchiveManager = SandboxedModule.require modulePath, requires: @requires, globals: @globals
  90. describe "archiveDoc", ->
  91. it "should use correct options", (done)->
  92. @request.put = sinon.stub().callsArgWith(1, null, {statusCode:200,headers:{etag:""}})
  93. @DocArchiveManager.archiveDoc @project_id, @mongoDocs[0], (err)=>
  94. opts = @request.put.args[0][0]
  95. assert.deepEqual(opts.aws, {key:@settings.docstore.s3.key, secret:@settings.docstore.s3.secret, bucket:@settings.docstore.s3.bucket})
  96. opts.body.should.equal JSON.stringify(
  97. lines: @mongoDocs[0].lines
  98. ranges: @mongoDocs[0].ranges
  99. schema_v: 1
  100. )
  101. opts.timeout.should.equal (30*1000)
  102. opts.uri.should.equal "https://#{@settings.docstore.s3.bucket}.s3.amazonaws.com/#{@project_id}/#{@mongoDocs[0]._id}"
  103. done()
  104. it "should return no md5 error", (done)->
  105. data = JSON.stringify(
  106. lines: @mongoDocs[0].lines
  107. ranges: @mongoDocs[0].ranges
  108. schema_v: 1
  109. )
  110. @md5 = crypto.createHash("md5").update(data).digest("hex")
  111. @request.put = sinon.stub().callsArgWith(1, null, {statusCode:200,headers:{etag:@md5}})
  112. @DocArchiveManager.archiveDoc @project_id, @mongoDocs[0], (err)=>
  113. should.not.exist err
  114. done()
  115. it "should return the error", (done)->
  116. @request.put = sinon.stub().callsArgWith(1, @stubbedError, {statusCode:400,headers:{etag:""}})
  117. @DocArchiveManager.archiveDoc @project_id, @mongoDocs[0], (err)=>
  118. should.exist err
  119. done()
  120. describe "unarchiveDoc", ->
  121. it "should use correct options", (done)->
  122. @request.get = sinon.stub().callsArgWith(1, null, statusCode:200, @mongoDocs[0].lines)
  123. @request.del = sinon.stub().callsArgWith(1, null, statusCode:204, {})
  124. @DocArchiveManager.unarchiveDoc @project_id, @mongoDocs[0]._id, (err)=>
  125. opts = @request.get.args[0][0]
  126. assert.deepEqual(opts.aws, {key:@settings.docstore.s3.key, secret:@settings.docstore.s3.secret, bucket:@settings.docstore.s3.bucket})
  127. opts.json.should.equal true
  128. opts.timeout.should.equal (30*1000)
  129. opts.uri.should.equal "https://#{@settings.docstore.s3.bucket}.s3.amazonaws.com/#{@project_id}/#{@mongoDocs[0]._id}"
  130. done()
  131. it "should return the error", (done)->
  132. @request.get = sinon.stub().callsArgWith(1, @stubbedError, {}, {})
  133. @DocArchiveManager.unarchiveDoc @project_id, @mongoDocs[0], (err)=>
  134. should.exist err
  135. done()
  136. it "should error if the doc lines are a string not an array", (done)->
  137. @request.get = sinon.stub().callsArgWith(1, null, statusCode:200, "this is a string")
  138. @request.del = sinon.stub()
  139. @DocArchiveManager.unarchiveDoc @project_id, @mongoDocs[0], (err)=>
  140. should.exist err
  141. @request.del.called.should.equal false
  142. done()
  143. describe "archiveAllDocs", ->
  144. it "should archive all project docs which are not in s3", (done)->
  145. @MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, @mongoDocs)
  146. @DocArchiveManager.archiveDoc = sinon.stub().callsArgWith(2, null)
  147. @DocArchiveManager.archiveAllDocs @project_id, (err)=>
  148. @DocArchiveManager.archiveDoc.calledWith(@project_id, @mongoDocs[0]).should.equal true
  149. @DocArchiveManager.archiveDoc.calledWith(@project_id, @mongoDocs[1]).should.equal true
  150. @DocArchiveManager.archiveDoc.calledWith(@project_id, @mongoDocs[4]).should.equal true
  151. @DocArchiveManager.archiveDoc.calledWith(@project_id, @mongoDocs[2]).should.equal false
  152. @DocArchiveManager.archiveDoc.calledWith(@project_id, @mongoDocs[3]).should.equal false
  153. should.not.exist err
  154. done()
  155. it "should return error if have no docs", (done)->
  156. @MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, null)
  157. @DocArchiveManager.archiveAllDocs @project_id, (err)=>
  158. should.exist err
  159. done()
  160. it "should return the error", (done)->
  161. @MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, @error, null)
  162. @DocArchiveManager.archiveAllDocs @project_id, (err)=>
  163. err.should.equal @error
  164. done()
  165. describe "when most have been already put in s3", ->
  166. beforeEach ->
  167. numberOfDocs = 10 * 1000
  168. @mongoDocs = []
  169. while --numberOfDocs != 0
  170. @mongoDocs.push({inS3:true, _id: ObjectId()})
  171. @MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, @mongoDocs)
  172. @DocArchiveManager.archiveDoc = sinon.stub().callsArgWith(2, null)
  173. it "should not throw and error", (done)->
  174. @DocArchiveManager.archiveAllDocs @project_id, (err)=>
  175. should.not.exist err
  176. done()
  177. describe "unArchiveAllDocs", ->
  178. it "should unarchive all inS3 docs", (done)->
  179. @MongoManager.getArchivedProjectDocs = sinon.stub().callsArgWith(1, null, @archivedDocs)
  180. @DocArchiveManager.unarchiveDoc = sinon.stub().callsArgWith(2, null)
  181. @DocArchiveManager.unArchiveAllDocs @project_id, (err)=>
  182. for doc in @archivedDocs
  183. @DocArchiveManager.unarchiveDoc.calledWith(@project_id, doc._id).should.equal true
  184. should.not.exist err
  185. done()
  186. it "should return error if have no docs", (done)->
  187. @MongoManager.getArchivedProjectDocs = sinon.stub().callsArgWith(1, null, null)
  188. @DocArchiveManager.unArchiveAllDocs @project_id, (err)=>
  189. should.exist err
  190. done()
  191. it "should return the error", (done)->
  192. @MongoManager.getArchivedProjectDocs = sinon.stub().callsArgWith(1, @error, null)
  193. @DocArchiveManager.unArchiveAllDocs @project_id, (err)=>
  194. err.should.equal @error
  195. done()
  196. describe "destroyAllDocs", ->
  197. beforeEach ->
  198. @request.del = sinon.stub().callsArgWith(1, null, statusCode:204, {})
  199. @MongoManager.getProjectsDocs = sinon.stub().callsArgWith(3, null, @mixedDocs)
  200. @MongoManager.findDoc = sinon.stub().callsArgWith(3, null, null)
  201. @MongoManager.destroyDoc = sinon.stub().yields()
  202. for doc in @mixedDocs
  203. @MongoManager.findDoc.withArgs(@project_id, doc._id).callsArgWith(3, null, doc)
  204. it "should destroy all the docs", (done)->
  205. @DocArchiveManager.destroyDoc = sinon.stub().callsArgWith(2, null)
  206. @DocArchiveManager.destroyAllDocs @project_id, (err)=>
  207. for doc in @mixedDocs
  208. @DocArchiveManager.destroyDoc.calledWith(@project_id, doc._id).should.equal true
  209. should.not.exist err
  210. done()
  211. it "should only the s3 docs from s3", (done)->
  212. docOpts = (doc) =>
  213. JSON.parse(JSON.stringify({
  214. aws: {key:@settings.docstore.s3.key, secret:@settings.docstore.s3.secret, bucket:@settings.docstore.s3.bucket},
  215. json: true,
  216. timeout: 30 * 1000
  217. uri:"https://#{@settings.docstore.s3.bucket}.s3.amazonaws.com/#{@project_id}/#{doc._id}"
  218. }))
  219. @DocArchiveManager.destroyAllDocs @project_id, (err)=>
  220. expect(err).not.to.exist
  221. for doc in @archivedDocs
  222. sinon.assert.calledWith(@request.del, docOpts(doc))
  223. for doc in @unarchivedDocs
  224. expect(@request.del.calledWith(docOpts(doc))).to.equal false # no notCalledWith
  225. done()
  226. it "should remove the docs from mongo", (done)->
  227. @DocArchiveManager.destroyAllDocs @project_id, (err)=>
  228. expect(err).not.to.exist
  229. for doc in @mixedDocs
  230. sinon.assert.calledWith(@MongoManager.destroyDoc, doc._id)
  231. done()
  232. describe "_s3DocToMongoDoc", ->
  233. describe "with the old schema", ->
  234. it "should return the docs lines", (done) ->
  235. @DocArchiveManager._s3DocToMongoDoc ["doc", "lines"], (error, doc) ->
  236. expect(doc).to.deep.equal {
  237. lines: ["doc", "lines"]
  238. }
  239. done()
  240. describe "with the new schema", ->
  241. it "should return the doc lines and ranges", (done) ->
  242. @RangeManager.jsonRangesToMongo = sinon.stub().returns {"mongo": "ranges"}
  243. @DocArchiveManager._s3DocToMongoDoc {
  244. lines: ["doc", "lines"]
  245. ranges: {"json": "ranges"}
  246. schema_v: 1
  247. }, (error, doc) ->
  248. expect(doc).to.deep.equal {
  249. lines: ["doc", "lines"]
  250. ranges: {"mongo": "ranges"}
  251. }
  252. done()
  253. it "should return just the doc lines when there are no ranges", (done) ->
  254. @DocArchiveManager._s3DocToMongoDoc {
  255. lines: ["doc", "lines"]
  256. schema_v: 1
  257. }, (error, doc) ->
  258. expect(doc).to.deep.equal {
  259. lines: ["doc", "lines"]
  260. }
  261. done()
  262. describe "with an unrecognised schema", ->
  263. it "should return an error", (done) ->
  264. @DocArchiveManager._s3DocToMongoDoc {
  265. schema_v: 2
  266. }, (error, doc) ->
  267. expect(error).to.exist
  268. done()
  269. describe "_mongoDocToS3Doc", ->
  270. describe "with a valid doc", ->
  271. it "should return the json version", (done) ->
  272. @DocArchiveManager._mongoDocToS3Doc doc = {
  273. lines: ["doc", "lines"]
  274. ranges: { "mock": "ranges" }
  275. }, (err, s3_doc) ->
  276. expect(s3_doc).to.equal JSON.stringify({
  277. lines: ["doc", "lines"]
  278. ranges: { "mock": "ranges" }
  279. schema_v: 1
  280. })
  281. done()
  282. describe "with null bytes in the result", ->
  283. beforeEach ->
  284. @_stringify = JSON.stringify
  285. JSON.stringify = sinon.stub().returns '{"bad": "\u0000"}'
  286. afterEach ->
  287. JSON.stringify = @_stringify
  288. it "should return an error", (done) ->
  289. @DocArchiveManager._mongoDocToS3Doc {
  290. lines: ["doc", "lines"]
  291. ranges: { "mock": "ranges" }
  292. }, (err, s3_doc) ->
  293. expect(err).to.exist
  294. done()
  295. describe "without doc lines", ->
  296. it "should return an error", (done) ->
  297. @DocArchiveManager._mongoDocToS3Doc {}, (err, s3_doc) ->
  298. expect(err).to.exist
  299. done()