HttpErrorHandler.test.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. import { vi, expect } from 'vitest'
  2. import MockResponse from '../helpers/MockResponse.mjs'
  3. import MockRequest from '../helpers/MockRequest.mjs'
  4. const modulePath = '../../../../app/src/Features/Errors/HttpErrorHandler.mjs'
  5. describe('HttpErrorHandler', function () {
  6. beforeEach(async function (ctx) {
  7. ctx.req = new MockRequest(vi)
  8. ctx.res = new MockResponse(vi)
  9. vi.doMock('@overleaf/settings', () => ({
  10. default: {
  11. appName: 'Overleaf',
  12. statusPageUrl: 'https://status.overlaf.com',
  13. },
  14. }))
  15. ctx.HttpErrorHandler = (await import(modulePath)).default
  16. })
  17. describe('handleErrorByStatusCode', function () {
  18. it('returns the http status code of 400 errors', function (ctx) {
  19. const err = new Error()
  20. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 400)
  21. expect(ctx.res.statusCode).to.equal(400)
  22. })
  23. it('returns the http status code of 500 errors', function (ctx) {
  24. const err = new Error()
  25. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 500)
  26. expect(ctx.res.statusCode).to.equal(500)
  27. })
  28. it('returns the http status code of any 5xx error', function (ctx) {
  29. const err = new Error()
  30. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 588)
  31. expect(ctx.res.statusCode).to.equal(588)
  32. })
  33. it('returns the http status code of any 4xx error', function (ctx) {
  34. const err = new Error()
  35. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 488)
  36. expect(ctx.res.statusCode).to.equal(488)
  37. })
  38. it('returns 500 for http status codes smaller than 400', function (ctx) {
  39. const err = new Error()
  40. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 302)
  41. expect(ctx.res.statusCode).to.equal(500)
  42. })
  43. it('returns 500 for http status codes larger than 600', function (ctx) {
  44. const err = new Error()
  45. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 302)
  46. expect(ctx.res.statusCode).to.equal(500)
  47. })
  48. it('returns 500 when the error has no http status code', function (ctx) {
  49. const err = new Error()
  50. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err)
  51. expect(ctx.res.statusCode).to.equal(500)
  52. })
  53. it('uses the conflict() error handler', function (ctx) {
  54. const err = new Error()
  55. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 409)
  56. expect(ctx.res.body).to.equal('conflict')
  57. })
  58. it('uses the forbidden() error handler', function (ctx) {
  59. const err = new Error()
  60. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 403)
  61. expect(ctx.res.body).to.equal('restricted')
  62. })
  63. it('uses the notFound() error handler', function (ctx) {
  64. const err = new Error()
  65. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 404)
  66. expect(ctx.res.body).to.equal('not found')
  67. })
  68. it('uses the unprocessableEntity() error handler', function (ctx) {
  69. const err = new Error()
  70. err.httpStatusCode = 422
  71. ctx.HttpErrorHandler.handleErrorByStatusCode(ctx.req, ctx.res, err, 422)
  72. expect(ctx.res.body).to.equal('unprocessable entity')
  73. })
  74. })
  75. describe('badRequest', function () {
  76. it('returns 400', function (ctx) {
  77. ctx.HttpErrorHandler.badRequest(ctx.req, ctx.res)
  78. expect(ctx.res.statusCode).to.equal(400)
  79. })
  80. it('should print a message when no content-type is included', function (ctx) {
  81. ctx.HttpErrorHandler.badRequest(ctx.req, ctx.res)
  82. expect(ctx.res.body).to.equal('client error')
  83. })
  84. it("should render a template including the error message when content-type is 'html'", function (ctx) {
  85. ctx.req.accepts = () => 'html'
  86. ctx.HttpErrorHandler.badRequest(ctx.req, ctx.res, 'an error')
  87. expect(ctx.res.renderedTemplate).to.equal('general/400')
  88. expect(ctx.res.renderedVariables).to.deep.equal({
  89. title: 'Client Error',
  90. message: 'an error',
  91. })
  92. })
  93. it("should render a default template when content-type is 'html' and no message is provided", function (ctx) {
  94. ctx.req.accepts = () => 'html'
  95. ctx.HttpErrorHandler.badRequest(ctx.req, ctx.res)
  96. expect(ctx.res.renderedTemplate).to.equal('general/400')
  97. expect(ctx.res.renderedVariables).to.deep.equal({
  98. title: 'Client Error',
  99. message: undefined,
  100. })
  101. })
  102. it("should return a json object when content-type is 'json'", function (ctx) {
  103. ctx.req.accepts = () => 'json'
  104. ctx.HttpErrorHandler.badRequest(ctx.req, ctx.res, 'an error', {
  105. foo: 'bar',
  106. })
  107. expect(JSON.parse(ctx.res.body)).to.deep.equal({
  108. message: 'an error',
  109. foo: 'bar',
  110. })
  111. })
  112. it("should return an empty json object when content-type is 'json' and no message and info are provided", function (ctx) {
  113. ctx.req.accepts = () => 'json'
  114. ctx.HttpErrorHandler.badRequest(ctx.req, ctx.res)
  115. expect(JSON.parse(ctx.res.body)).to.deep.equal({})
  116. })
  117. })
  118. describe('conflict', function () {
  119. it('returns 409', function (ctx) {
  120. ctx.HttpErrorHandler.conflict(ctx.req, ctx.res)
  121. expect(ctx.res.statusCode).to.equal(409)
  122. })
  123. it('should print a message when no content-type is included', function (ctx) {
  124. ctx.HttpErrorHandler.conflict(ctx.req, ctx.res)
  125. expect(ctx.res.body).to.equal('conflict')
  126. })
  127. it("should render a template including the error message when content-type is 'html'", function (ctx) {
  128. ctx.req.accepts = () => 'html'
  129. ctx.HttpErrorHandler.unprocessableEntity(ctx.req, ctx.res, 'an error')
  130. expect(ctx.res.renderedTemplate).to.equal('general/400')
  131. expect(ctx.res.renderedVariables).to.deep.equal({
  132. title: 'Client Error',
  133. message: 'an error',
  134. })
  135. })
  136. it("should return a json object when content-type is 'json'", function (ctx) {
  137. ctx.req.accepts = () => 'json'
  138. ctx.HttpErrorHandler.unprocessableEntity(ctx.req, ctx.res, 'an error', {
  139. foo: 'bar',
  140. })
  141. expect(JSON.parse(ctx.res.body)).to.deep.equal({
  142. message: 'an error',
  143. foo: 'bar',
  144. })
  145. })
  146. })
  147. describe('forbidden', function () {
  148. it('returns 403', function (ctx) {
  149. ctx.HttpErrorHandler.forbidden(ctx.req, ctx.res)
  150. expect(ctx.res.statusCode).to.equal(403)
  151. })
  152. it('should print a message when no content-type is included', function (ctx) {
  153. ctx.HttpErrorHandler.forbidden(ctx.req, ctx.res)
  154. expect(ctx.res.body).to.equal('restricted')
  155. })
  156. it("should render a template when content-type is 'html'", function (ctx) {
  157. ctx.req.accepts = () => 'html'
  158. ctx.HttpErrorHandler.forbidden(ctx.req, ctx.res)
  159. expect(ctx.res.renderedTemplate).to.equal('user/restricted')
  160. expect(ctx.res.renderedVariables).to.deep.equal({
  161. title: 'restricted',
  162. })
  163. })
  164. it("should return a json object when content-type is 'json'", function (ctx) {
  165. ctx.req.accepts = () => 'json'
  166. ctx.HttpErrorHandler.forbidden(ctx.req, ctx.res, 'an error', {
  167. foo: 'bar',
  168. })
  169. expect(JSON.parse(ctx.res.body)).to.deep.equal({
  170. message: 'an error',
  171. foo: 'bar',
  172. })
  173. })
  174. })
  175. describe('notFound', function () {
  176. it('returns 404', function (ctx) {
  177. ctx.HttpErrorHandler.notFound(ctx.req, ctx.res)
  178. expect(ctx.res.statusCode).to.equal(404)
  179. })
  180. it('should print a message when no content-type is included', function (ctx) {
  181. ctx.HttpErrorHandler.notFound(ctx.req, ctx.res)
  182. expect(ctx.res.body).to.equal('not found')
  183. })
  184. it("should render a template when content-type is 'html'", function (ctx) {
  185. ctx.req.accepts = () => 'html'
  186. ctx.HttpErrorHandler.notFound(ctx.req, ctx.res)
  187. expect(ctx.res.renderedTemplate).to.equal('general/404')
  188. expect(ctx.res.renderedVariables).to.deep.equal({
  189. title: 'page_not_found',
  190. })
  191. })
  192. it("should return a json object when content-type is 'json'", function (ctx) {
  193. ctx.req.accepts = () => 'json'
  194. ctx.HttpErrorHandler.notFound(ctx.req, ctx.res, 'an error', {
  195. foo: 'bar',
  196. })
  197. expect(JSON.parse(ctx.res.body)).to.deep.equal({
  198. message: 'an error',
  199. foo: 'bar',
  200. })
  201. })
  202. })
  203. describe('unprocessableEntity', function () {
  204. it('returns 422', function (ctx) {
  205. ctx.HttpErrorHandler.unprocessableEntity(ctx.req, ctx.res)
  206. expect(ctx.res.statusCode).to.equal(422)
  207. })
  208. it('should print a message when no content-type is included', function (ctx) {
  209. ctx.HttpErrorHandler.unprocessableEntity(ctx.req, ctx.res)
  210. expect(ctx.res.body).to.equal('unprocessable entity')
  211. })
  212. it("should render a template including the error message when content-type is 'html'", function (ctx) {
  213. ctx.req.accepts = () => 'html'
  214. ctx.HttpErrorHandler.unprocessableEntity(ctx.req, ctx.res, 'an error')
  215. expect(ctx.res.renderedTemplate).to.equal('general/400')
  216. expect(ctx.res.renderedVariables).to.deep.equal({
  217. title: 'Client Error',
  218. message: 'an error',
  219. })
  220. })
  221. it("should return a json object when content-type is 'json'", function (ctx) {
  222. ctx.req.accepts = () => 'json'
  223. ctx.HttpErrorHandler.unprocessableEntity(ctx.req, ctx.res, 'an error', {
  224. foo: 'bar',
  225. })
  226. expect(JSON.parse(ctx.res.body)).to.deep.equal({
  227. message: 'an error',
  228. foo: 'bar',
  229. })
  230. })
  231. describe('legacyInternal', function () {
  232. it('returns 500', function (ctx) {
  233. ctx.HttpErrorHandler.legacyInternal(ctx.req, ctx.res, new Error())
  234. expect(ctx.res.statusCode).to.equal(500)
  235. })
  236. it('should send the error to the logger', function (ctx) {
  237. const error = new Error('message')
  238. ctx.HttpErrorHandler.legacyInternal(ctx.req, ctx.res, 'message', error)
  239. expect(ctx.req.logger.setLevel).toHaveBeenCalledWith('error')
  240. expect(ctx.req.logger.addFields).toHaveBeenCalledWith({
  241. err: error,
  242. })
  243. })
  244. it('should print a message when no content-type is included', function (ctx) {
  245. ctx.HttpErrorHandler.legacyInternal(ctx.req, ctx.res, new Error())
  246. expect(ctx.res.body).to.equal('internal server error')
  247. })
  248. it("should render a template when content-type is 'html'", function (ctx) {
  249. ctx.req.accepts = () => 'html'
  250. ctx.HttpErrorHandler.legacyInternal(ctx.req, ctx.res, new Error())
  251. expect(ctx.res.renderedTemplate).to.equal('general/500')
  252. expect(ctx.res.renderedVariables).to.deep.equal({
  253. title: 'Server Error',
  254. })
  255. })
  256. it("should return a json object with a static message when content-type is 'json'", function (ctx) {
  257. ctx.req.accepts = () => 'json'
  258. ctx.HttpErrorHandler.legacyInternal(
  259. ctx.req,
  260. ctx.res,
  261. 'a message',
  262. new Error()
  263. )
  264. expect(JSON.parse(ctx.res.body)).to.deep.equal({
  265. message: 'a message',
  266. })
  267. })
  268. })
  269. })
  270. })