RequestParser.test.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import { vi, expect, describe, beforeEach, afterEach, it } from 'vitest'
  2. import sinon from 'sinon'
  3. import tk from 'timekeeper'
  4. import path from 'node:path'
  5. const modulePath = path.join(
  6. import.meta.dirname,
  7. '../../../app/js/RequestParser'
  8. )
  9. describe('RequestParser', () => {
  10. beforeEach(async ctx => {
  11. tk.freeze()
  12. ctx.callback = sinon.stub()
  13. ctx.validResource = {
  14. path: 'main.tex',
  15. date: '12:00 01/02/03',
  16. content: 'Hello world',
  17. }
  18. ctx.validRequest = {
  19. compile: {
  20. token: 'token-123',
  21. options: {
  22. imageName: 'basicImageName/here:2017-1',
  23. compiler: 'pdflatex',
  24. timeout: 42,
  25. },
  26. resources: [],
  27. },
  28. }
  29. vi.doMock('@overleaf/settings', () => ({
  30. default: (ctx.settings = {}),
  31. }))
  32. vi.doMock('../../../app/js/OutputCacheManager', () => ({
  33. default: { BUILD_REGEX: /^[0-9a-f]+-[0-9a-f]+$/ },
  34. }))
  35. ctx.RequestParser = (await import(modulePath)).default
  36. })
  37. afterEach(() => {
  38. tk.reset()
  39. })
  40. describe('without a top level object', () => {
  41. beforeEach(ctx => {
  42. ctx.RequestParser.parse([], ctx.callback)
  43. })
  44. it('should return an error', ctx => {
  45. expect(ctx.callback).to.have.been.called
  46. expect(ctx.callback.args[0][0].message).to.equal(
  47. 'top level object should have a compile attribute'
  48. )
  49. })
  50. })
  51. describe('without a compile attribute', () => {
  52. beforeEach(ctx => {
  53. ctx.RequestParser.parse({}, ctx.callback)
  54. })
  55. it('should return an error', ctx => {
  56. expect(ctx.callback).to.have.been.called
  57. expect(ctx.callback.args[0][0].message).to.equal(
  58. 'top level object should have a compile attribute'
  59. )
  60. })
  61. })
  62. describe('without a valid compiler', () => {
  63. beforeEach(ctx => {
  64. ctx.validRequest.compile.options.compiler = 'not-a-compiler'
  65. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  66. })
  67. it('should return an error', ctx => {
  68. ctx.callback
  69. .calledWithMatch({
  70. message:
  71. 'compiler attribute should be one of: pdflatex, latex, xelatex, lualatex',
  72. })
  73. .should.equal(true)
  74. })
  75. })
  76. describe('without a compiler specified', () => {
  77. beforeEach(async ctx => {
  78. await new Promise((resolve, reject) => {
  79. delete ctx.validRequest.compile.options.compiler
  80. ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
  81. if (error) return reject(error)
  82. ctx.data = data
  83. resolve()
  84. })
  85. })
  86. })
  87. it('should set the compiler to pdflatex by default', ctx => {
  88. ctx.data.compiler.should.equal('pdflatex')
  89. })
  90. })
  91. describe('with imageName set', () => {
  92. beforeEach(async ctx => {
  93. await new Promise((resolve, reject) => {
  94. ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
  95. if (error) return reject(error)
  96. ctx.data = data
  97. resolve()
  98. })
  99. })
  100. })
  101. it('should set the imageName', ctx => {
  102. ctx.data.imageName.should.equal('basicImageName/here:2017-1')
  103. })
  104. })
  105. describe('when image restrictions are present', () => {
  106. beforeEach(ctx => {
  107. ctx.settings.clsi = { docker: {} }
  108. ctx.settings.clsi.docker.allowedImages = [
  109. 'repo/name:tag1',
  110. 'repo/name:tag2',
  111. ]
  112. })
  113. describe('with imageName set to something invalid', () => {
  114. beforeEach(ctx => {
  115. const request = ctx.validRequest
  116. request.compile.options.imageName = 'something/different:latest'
  117. ctx.RequestParser.parse(request, (error, data) => {
  118. ctx.error = error
  119. ctx.data = data
  120. })
  121. })
  122. it('should throw an error for imageName', ctx => {
  123. expect(String(ctx.error)).to.include(
  124. 'imageName attribute should be one of'
  125. )
  126. })
  127. })
  128. describe('with imageName set to something valid', () => {
  129. beforeEach(ctx => {
  130. const request = ctx.validRequest
  131. request.compile.options.imageName = 'repo/name:tag1'
  132. ctx.RequestParser.parse(request, (error, data) => {
  133. ctx.error = error
  134. ctx.data = data
  135. })
  136. })
  137. it('should set the imageName', ctx => {
  138. ctx.data.imageName.should.equal('repo/name:tag1')
  139. })
  140. })
  141. })
  142. describe('with flags set', () => {
  143. beforeEach(async ctx => {
  144. await new Promise((resolve, reject) => {
  145. ctx.validRequest.compile.options.flags = ['-file-line-error']
  146. ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
  147. if (error) return reject(error)
  148. ctx.data = data
  149. resolve()
  150. })
  151. })
  152. })
  153. it('should set the flags attribute', ctx => {
  154. expect(ctx.data.flags).to.deep.equal(['-file-line-error'])
  155. })
  156. })
  157. describe('with flags not specified', () => {
  158. beforeEach(async ctx => {
  159. await new Promise((resolve, reject) => {
  160. ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
  161. if (error) return reject(error)
  162. ctx.data = data
  163. resolve()
  164. })
  165. })
  166. })
  167. it('it should have an empty flags list', ctx => {
  168. expect(ctx.data.flags).to.deep.equal([])
  169. })
  170. })
  171. describe('without a timeout specified', () => {
  172. beforeEach(async ctx => {
  173. await new Promise((resolve, reject) => {
  174. delete ctx.validRequest.compile.options.timeout
  175. ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
  176. if (error) return reject(error)
  177. ctx.data = data
  178. resolve()
  179. })
  180. })
  181. })
  182. it('should set the timeout to MAX_TIMEOUT', ctx => {
  183. ctx.data.timeout.should.equal(ctx.RequestParser.MAX_TIMEOUT * 1000)
  184. })
  185. })
  186. describe('with a timeout larger than the maximum', () => {
  187. beforeEach(async ctx => {
  188. await new Promise((resolve, reject) => {
  189. ctx.validRequest.compile.options.timeout =
  190. ctx.RequestParser.MAX_TIMEOUT + 1
  191. ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
  192. if (error) return reject(error)
  193. ctx.data = data
  194. resolve()
  195. })
  196. })
  197. })
  198. it('should set the timeout to MAX_TIMEOUT', ctx => {
  199. ctx.data.timeout.should.equal(ctx.RequestParser.MAX_TIMEOUT * 1000)
  200. })
  201. })
  202. describe('with a timeout', () => {
  203. beforeEach(async ctx => {
  204. await new Promise((resolve, reject) => {
  205. ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
  206. if (error) return reject(error)
  207. ctx.data = data
  208. resolve()
  209. })
  210. })
  211. })
  212. it('should set the timeout (in milliseconds)', ctx => {
  213. ctx.data.timeout.should.equal(
  214. ctx.validRequest.compile.options.timeout * 1000
  215. )
  216. })
  217. })
  218. describe('with a resource without a path', () => {
  219. beforeEach(ctx => {
  220. delete ctx.validResource.path
  221. ctx.validRequest.compile.resources.push(ctx.validResource)
  222. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  223. })
  224. it('should return an error', ctx => {
  225. ctx.callback
  226. .calledWithMatch({
  227. message: 'all resources should have a path attribute',
  228. })
  229. .should.equal(true)
  230. })
  231. })
  232. describe('with a resource with a path', () => {
  233. beforeEach(ctx => {
  234. ctx.validResource.path = ctx.path = 'test.tex'
  235. ctx.validRequest.compile.resources.push(ctx.validResource)
  236. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  237. ctx.data = ctx.callback.args[0][1]
  238. })
  239. it('should return the path in the parsed response', ctx => {
  240. ctx.data.resources[0].path.should.equal(ctx.path)
  241. })
  242. })
  243. describe('with a resource with a malformed modified date', () => {
  244. beforeEach(ctx => {
  245. ctx.validResource.modified = 'not-a-date'
  246. ctx.validRequest.compile.resources.push(ctx.validResource)
  247. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  248. })
  249. it('should return an error', ctx => {
  250. ctx.callback
  251. .calledWithMatch({
  252. message:
  253. 'resource modified date could not be understood: ' +
  254. ctx.validResource.modified,
  255. })
  256. .should.equal(true)
  257. })
  258. })
  259. describe('with a valid buildId', () => {
  260. beforeEach(async ctx => {
  261. await new Promise((resolve, reject) => {
  262. ctx.validRequest.compile.options.buildId =
  263. '195a4869176-a4ad60bee7bf35e4'
  264. ctx.RequestParser.parse(ctx.validRequest, (error, data) => {
  265. if (error) return reject(error)
  266. ctx.data = data
  267. resolve()
  268. })
  269. })
  270. })
  271. it('should return an error', ctx => {
  272. ctx.data.buildId.should.equal('195a4869176-a4ad60bee7bf35e4')
  273. })
  274. })
  275. describe('with a bad buildId', () => {
  276. beforeEach(ctx => {
  277. ctx.validRequest.compile.options.buildId = 'foo/bar'
  278. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  279. })
  280. it('should return an error', ctx => {
  281. ctx.callback
  282. .calledWithMatch({
  283. message:
  284. 'buildId attribute does not match regex /^[0-9a-f]+-[0-9a-f]+$/',
  285. })
  286. .should.equal(true)
  287. })
  288. })
  289. describe('with a resource with a valid date', () => {
  290. beforeEach(ctx => {
  291. ctx.date = '12:00 01/02/03'
  292. ctx.validResource.modified = ctx.date
  293. ctx.validRequest.compile.resources.push(ctx.validResource)
  294. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  295. ctx.data = ctx.callback.args[0][1]
  296. })
  297. it('should return the date as a Javascript Date object', ctx => {
  298. ;(ctx.data.resources[0].modified instanceof Date).should.equal(true)
  299. ctx.data.resources[0].modified
  300. .getTime()
  301. .should.equal(Date.parse(ctx.date))
  302. })
  303. })
  304. describe('with a resource without either a content or URL attribute', () => {
  305. beforeEach(ctx => {
  306. delete ctx.validResource.url
  307. delete ctx.validResource.content
  308. ctx.validRequest.compile.resources.push(ctx.validResource)
  309. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  310. })
  311. it('should return an error', ctx => {
  312. ctx.callback
  313. .calledWithMatch({
  314. message:
  315. 'all resources should have either a url or content attribute',
  316. })
  317. .should.equal(true)
  318. })
  319. })
  320. describe('with a resource where the content is not a string', () => {
  321. beforeEach(ctx => {
  322. ctx.validResource.content = []
  323. ctx.validRequest.compile.resources.push(ctx.validResource)
  324. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  325. })
  326. it('should return an error', ctx => {
  327. ctx.callback
  328. .calledWithMatch({ message: 'content attribute should be a string' })
  329. .should.equal(true)
  330. })
  331. })
  332. describe('with a resource where the url is not a string', () => {
  333. beforeEach(ctx => {
  334. ctx.validResource.url = []
  335. ctx.validRequest.compile.resources.push(ctx.validResource)
  336. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  337. })
  338. it('should return an error', ctx => {
  339. ctx.callback
  340. .calledWithMatch({ message: 'url attribute should be a string' })
  341. .should.equal(true)
  342. })
  343. })
  344. describe('with a resource with a url', () => {
  345. beforeEach(ctx => {
  346. ctx.validResource.url = ctx.url = 'www.example.com'
  347. ctx.validRequest.compile.resources.push(ctx.validResource)
  348. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  349. ctx.data = ctx.callback.args[0][1]
  350. })
  351. it('should return the url in the parsed response', ctx => {
  352. ctx.data.resources[0].url.should.equal(ctx.url)
  353. })
  354. })
  355. describe('with a resource with a content attribute', () => {
  356. beforeEach(ctx => {
  357. ctx.validResource.content = ctx.content = 'Hello world'
  358. ctx.validRequest.compile.resources.push(ctx.validResource)
  359. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  360. ctx.data = ctx.callback.args[0][1]
  361. })
  362. it('should return the content in the parsed response', ctx => {
  363. ctx.data.resources[0].content.should.equal(ctx.content)
  364. })
  365. })
  366. describe('without a root resource path', () => {
  367. beforeEach(ctx => {
  368. delete ctx.validRequest.compile.rootResourcePath
  369. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  370. ctx.data = ctx.callback.args[0][1]
  371. })
  372. it("should set the root resource path to 'main.tex' by default", ctx => {
  373. ctx.data.rootResourcePath.should.equal('main.tex')
  374. })
  375. })
  376. describe('with a root resource path', () => {
  377. beforeEach(ctx => {
  378. ctx.validRequest.compile.rootResourcePath = ctx.path = 'test.tex'
  379. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  380. ctx.data = ctx.callback.args[0][1]
  381. })
  382. it('should return the root resource path in the parsed response', ctx => {
  383. ctx.data.rootResourcePath.should.equal(ctx.path)
  384. })
  385. })
  386. describe('with a root resource path that is not a string', () => {
  387. beforeEach(ctx => {
  388. ctx.validRequest.compile.rootResourcePath = []
  389. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  390. })
  391. it('should return an error', ctx => {
  392. ctx.callback
  393. .calledWithMatch({
  394. message: 'rootResourcePath attribute should be a string',
  395. })
  396. .should.equal(true)
  397. })
  398. })
  399. describe('with a root resource path that has a relative path', () => {
  400. beforeEach(ctx => {
  401. ctx.validRequest.compile.rootResourcePath = 'foo/../../bar.tex'
  402. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  403. ctx.data = ctx.callback.args[0][1]
  404. })
  405. it('should return an error', ctx => {
  406. ctx.callback
  407. .calledWithMatch({ message: 'relative path in root resource' })
  408. .should.equal(true)
  409. })
  410. })
  411. describe('with a root resource path that has unescaped + relative path', () => {
  412. beforeEach(ctx => {
  413. ctx.validRequest.compile.rootResourcePath = 'foo/../bar.tex'
  414. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  415. ctx.data = ctx.callback.args[0][1]
  416. })
  417. it('should return an error', ctx => {
  418. ctx.callback
  419. .calledWithMatch({ message: 'relative path in root resource' })
  420. .should.equal(true)
  421. })
  422. })
  423. describe('with an unknown syncType', () => {
  424. beforeEach(ctx => {
  425. ctx.validRequest.compile.options.syncType = 'unexpected'
  426. ctx.RequestParser.parse(ctx.validRequest, ctx.callback)
  427. ctx.data = ctx.callback.args[0][1]
  428. })
  429. it('should return an error', ctx => {
  430. ctx.callback
  431. .calledWithMatch({
  432. message:
  433. 'syncType attribute should be one of: full, incremental, history-full, history-incremental',
  434. })
  435. .should.equal(true)
  436. })
  437. })
  438. })