CompileManagerTests.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. const sinon = require('sinon')
  2. const modulePath = '../../../../app/src/Features/Compile/CompileManager.js'
  3. const SandboxedModule = require('sandboxed-module')
  4. describe('CompileManager', function () {
  5. beforeEach(function () {
  6. this.rateLimitGetStub = sinon.stub()
  7. this.ratelimiter = { addCount: sinon.stub() }
  8. this.timer = {
  9. done: sinon.stub(),
  10. }
  11. this.Metrics = {
  12. Timer: sinon.stub().returns(this.timer),
  13. inc: sinon.stub(),
  14. }
  15. this.CompileManager = SandboxedModule.require(modulePath, {
  16. requires: {
  17. '@overleaf/settings': (this.settings = {
  18. redis: { web: { host: 'localhost', port: 42 } },
  19. rateLimit: { autoCompile: {} },
  20. }),
  21. '../../infrastructure/RedisWrapper': {
  22. client: () => (this.rclient = { auth() {} }),
  23. },
  24. '../Project/ProjectRootDocManager': (this.ProjectRootDocManager = {}),
  25. '../Project/ProjectGetter': (this.ProjectGetter = {}),
  26. '../User/UserGetter': (this.UserGetter = {}),
  27. './ClsiManager': (this.ClsiManager = {}),
  28. '../../infrastructure/RateLimiter': this.ratelimiter,
  29. '@overleaf/metrics': this.Metrics,
  30. },
  31. })
  32. this.project_id = 'mock-project-id-123'
  33. this.user_id = 'mock-user-id-123'
  34. this.callback = sinon.stub()
  35. this.limits = {
  36. timeout: 42,
  37. compileGroup: 'standard',
  38. }
  39. })
  40. describe('compile', function () {
  41. beforeEach(function () {
  42. this.CompileManager._checkIfRecentlyCompiled = sinon
  43. .stub()
  44. .callsArgWith(2, null, false)
  45. this.ProjectRootDocManager.ensureRootDocumentIsSet = sinon
  46. .stub()
  47. .callsArgWith(1, null)
  48. this.CompileManager.getProjectCompileLimits = sinon
  49. .stub()
  50. .callsArgWith(1, null, this.limits)
  51. this.ClsiManager.sendRequest = sinon
  52. .stub()
  53. .callsArgWith(
  54. 3,
  55. null,
  56. (this.status = 'mock-status'),
  57. (this.outputFiles = 'mock output files'),
  58. (this.output = 'mock output')
  59. )
  60. })
  61. describe('succesfully', function () {
  62. beforeEach(function () {
  63. this.CompileManager._checkIfAutoCompileLimitHasBeenHit = (
  64. isAutoCompile,
  65. compileGroup,
  66. cb
  67. ) => cb(null, true)
  68. this.CompileManager.compile(
  69. this.project_id,
  70. this.user_id,
  71. {},
  72. this.callback
  73. )
  74. })
  75. it('should check the project has not been recently compiled', function () {
  76. this.CompileManager._checkIfRecentlyCompiled
  77. .calledWith(this.project_id, this.user_id)
  78. .should.equal(true)
  79. })
  80. it('should ensure that the root document is set', function () {
  81. this.ProjectRootDocManager.ensureRootDocumentIsSet
  82. .calledWith(this.project_id)
  83. .should.equal(true)
  84. })
  85. it('should get the project compile limits', function () {
  86. this.CompileManager.getProjectCompileLimits
  87. .calledWith(this.project_id)
  88. .should.equal(true)
  89. })
  90. it('should run the compile with the compile limits', function () {
  91. this.ClsiManager.sendRequest
  92. .calledWith(this.project_id, this.user_id, {
  93. timeout: this.limits.timeout,
  94. compileGroup: 'standard',
  95. })
  96. .should.equal(true)
  97. })
  98. it('should call the callback with the output', function () {
  99. this.callback
  100. .calledWith(null, this.status, this.outputFiles, this.output)
  101. .should.equal(true)
  102. })
  103. it('should time the compile', function () {
  104. this.timer.done.called.should.equal(true)
  105. })
  106. })
  107. describe('when the project has been recently compiled', function () {
  108. it('should return', function (done) {
  109. this.CompileManager._checkIfAutoCompileLimitHasBeenHit = (
  110. isAutoCompile,
  111. compileGroup,
  112. cb
  113. ) => cb(null, true)
  114. this.CompileManager._checkIfRecentlyCompiled = sinon
  115. .stub()
  116. .callsArgWith(2, null, true)
  117. this.CompileManager.compile(
  118. this.project_id,
  119. this.user_id,
  120. {},
  121. (err, status) => {
  122. if (err) {
  123. return done(err)
  124. }
  125. status.should.equal('too-recently-compiled')
  126. done()
  127. }
  128. )
  129. })
  130. })
  131. describe('should check the rate limit', function () {
  132. it('should return', function (done) {
  133. this.CompileManager._checkIfAutoCompileLimitHasBeenHit = sinon
  134. .stub()
  135. .callsArgWith(2, null, false)
  136. this.CompileManager.compile(
  137. this.project_id,
  138. this.user_id,
  139. {},
  140. (err, status) => {
  141. if (err) {
  142. return done(err)
  143. }
  144. status.should.equal('autocompile-backoff')
  145. done()
  146. }
  147. )
  148. })
  149. })
  150. })
  151. describe('getProjectCompileLimits', function () {
  152. beforeEach(function () {
  153. this.features = {
  154. compileTimeout: (this.timeout = 42),
  155. compileGroup: (this.group = 'priority'),
  156. }
  157. this.ProjectGetter.getProject = sinon
  158. .stub()
  159. .callsArgWith(
  160. 2,
  161. null,
  162. (this.project = { owner_ref: (this.owner_id = 'owner-id-123') })
  163. )
  164. this.UserGetter.getUser = sinon
  165. .stub()
  166. .callsArgWith(2, null, (this.user = { features: this.features }))
  167. this.CompileManager.getProjectCompileLimits(
  168. this.project_id,
  169. this.callback
  170. )
  171. })
  172. it('should look up the owner of the project', function () {
  173. this.ProjectGetter.getProject
  174. .calledWith(this.project_id, { owner_ref: 1 })
  175. .should.equal(true)
  176. })
  177. it("should look up the owner's features", function () {
  178. this.UserGetter.getUser
  179. .calledWith(this.project.owner_ref, {
  180. alphaProgram: 1,
  181. betaProgram: 1,
  182. features: 1,
  183. })
  184. .should.equal(true)
  185. })
  186. it('should return the limits', function () {
  187. this.callback
  188. .calledWith(null, {
  189. timeout: this.timeout,
  190. compileGroup: this.group,
  191. })
  192. .should.equal(true)
  193. })
  194. })
  195. describe('deleteAuxFiles', function () {
  196. beforeEach(function () {
  197. this.CompileManager.getProjectCompileLimits = sinon
  198. .stub()
  199. .callsArgWith(
  200. 1,
  201. null,
  202. (this.limits = { compileGroup: 'mock-compile-group' })
  203. )
  204. this.ClsiManager.deleteAuxFiles = sinon.stub().callsArg(3)
  205. this.CompileManager.deleteAuxFiles(
  206. this.project_id,
  207. this.user_id,
  208. this.callback
  209. )
  210. })
  211. it('should look up the compile group to use', function () {
  212. this.CompileManager.getProjectCompileLimits
  213. .calledWith(this.project_id)
  214. .should.equal(true)
  215. })
  216. it('should delete the aux files', function () {
  217. this.ClsiManager.deleteAuxFiles
  218. .calledWith(this.project_id, this.user_id, this.limits)
  219. .should.equal(true)
  220. })
  221. it('should call the callback', function () {
  222. this.callback.called.should.equal(true)
  223. })
  224. })
  225. describe('_checkIfRecentlyCompiled', function () {
  226. describe('when the key exists in redis', function () {
  227. beforeEach(function () {
  228. this.rclient.set = sinon.stub().callsArgWith(5, null, null)
  229. this.CompileManager._checkIfRecentlyCompiled(
  230. this.project_id,
  231. this.user_id,
  232. this.callback
  233. )
  234. })
  235. it('should try to set the key', function () {
  236. this.rclient.set
  237. .calledWith(
  238. `compile:${this.project_id}:${this.user_id}`,
  239. true,
  240. 'EX',
  241. this.CompileManager.COMPILE_DELAY,
  242. 'NX'
  243. )
  244. .should.equal(true)
  245. })
  246. it('should call the callback with true', function () {
  247. this.callback.calledWith(null, true).should.equal(true)
  248. })
  249. })
  250. describe('when the key does not exist in redis', function () {
  251. beforeEach(function () {
  252. this.rclient.set = sinon.stub().callsArgWith(5, null, 'OK')
  253. this.CompileManager._checkIfRecentlyCompiled(
  254. this.project_id,
  255. this.user_id,
  256. this.callback
  257. )
  258. })
  259. it('should try to set the key', function () {
  260. this.rclient.set
  261. .calledWith(
  262. `compile:${this.project_id}:${this.user_id}`,
  263. true,
  264. 'EX',
  265. this.CompileManager.COMPILE_DELAY,
  266. 'NX'
  267. )
  268. .should.equal(true)
  269. })
  270. it('should call the callback with false', function () {
  271. this.callback.calledWith(null, false).should.equal(true)
  272. })
  273. })
  274. })
  275. describe('_checkIfAutoCompileLimitHasBeenHit', function () {
  276. it('should be able to compile if it is not an autocompile', function (done) {
  277. this.ratelimiter.addCount.callsArgWith(2, null, true)
  278. this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  279. false,
  280. 'everyone',
  281. (err, canCompile) => {
  282. if (err) {
  283. return done(err)
  284. }
  285. canCompile.should.equal(true)
  286. done()
  287. }
  288. )
  289. })
  290. it('should be able to compile if rate limit has remianing', function (done) {
  291. this.ratelimiter.addCount.callsArgWith(1, null, true)
  292. this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  293. true,
  294. 'everyone',
  295. (err, canCompile) => {
  296. if (err) {
  297. return done(err)
  298. }
  299. const args = this.ratelimiter.addCount.args[0][0]
  300. args.throttle.should.equal(25)
  301. args.subjectName.should.equal('everyone')
  302. args.timeInterval.should.equal(20)
  303. args.endpointName.should.equal('auto_compile')
  304. canCompile.should.equal(true)
  305. done()
  306. }
  307. )
  308. })
  309. it('should be not able to compile if rate limit has no remianing', function (done) {
  310. this.ratelimiter.addCount.callsArgWith(1, null, false)
  311. this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  312. true,
  313. 'everyone',
  314. (err, canCompile) => {
  315. if (err) {
  316. return done(err)
  317. }
  318. canCompile.should.equal(false)
  319. done()
  320. }
  321. )
  322. })
  323. it('should return false if there is an error in the rate limit', function (done) {
  324. this.ratelimiter.addCount.callsArgWith(1, 'error')
  325. this.CompileManager._checkIfAutoCompileLimitHasBeenHit(
  326. true,
  327. 'everyone',
  328. (err, canCompile) => {
  329. if (err) {
  330. return done(err)
  331. }
  332. canCompile.should.equal(false)
  333. done()
  334. }
  335. )
  336. })
  337. })
  338. describe('wordCount', function () {
  339. beforeEach(function () {
  340. this.CompileManager.getProjectCompileLimits = sinon
  341. .stub()
  342. .callsArgWith(
  343. 1,
  344. null,
  345. (this.limits = { compileGroup: 'mock-compile-group' })
  346. )
  347. this.ClsiManager.wordCount = sinon.stub().callsArg(4)
  348. this.CompileManager.wordCount(
  349. this.project_id,
  350. this.user_id,
  351. false,
  352. this.callback
  353. )
  354. })
  355. it('should look up the compile group to use', function () {
  356. this.CompileManager.getProjectCompileLimits
  357. .calledWith(this.project_id)
  358. .should.equal(true)
  359. })
  360. it('should call wordCount for project', function () {
  361. this.ClsiManager.wordCount
  362. .calledWith(this.project_id, this.user_id, false, this.limits)
  363. .should.equal(true)
  364. })
  365. it('should call the callback', function () {
  366. this.callback.called.should.equal(true)
  367. })
  368. })
  369. })