strong-tie/inbound-calls
0
1'use strict'2 3const { test } = require('node:test')4const Joi = require('joi')5const Fastify = require('..')6 7const schema = {8 body: {9 type: 'object',10 properties: {11 name: { type: 'string' },12 work: { type: 'string' }13 },14 required: ['name', 'work']15 }16}17 18function echoBody (req, reply) {19 reply.code(200).send(req.body.name)20}21 22test('should work with valid payload', async (t) => {23 t.plan(2)24 25 const fastify = Fastify()26 27 fastify.post('/', { schema }, echoBody)28 29 const response = await fastify.inject({30 method: 'POST',31 payload: {32 name: 'michelangelo',33 work: 'sculptor, painter, architect and poet'34 },35 url: '/'36 })37 t.assert.deepStrictEqual(response.payload, 'michelangelo')38 t.assert.strictEqual(response.statusCode, 200)39})40 41test('should fail immediately with invalid payload', async (t) => {42 t.plan(2)43 44 const fastify = Fastify()45 46 fastify.post('/', { schema }, echoBody)47 48 const response = await fastify.inject({49 method: 'POST',50 payload: {51 hello: 'michelangelo'52 },53 url: '/'54 })55 56 t.assert.deepStrictEqual(response.json(), {57 statusCode: 400,58 code: 'FST_ERR_VALIDATION',59 error: 'Bad Request',60 message: "body must have required property 'name'"61 })62 t.assert.strictEqual(response.statusCode, 400)63})64 65test('should be able to use setErrorHandler specify custom validation error', async (t) => {66 t.plan(2)67 68 const fastify = Fastify()69 70 fastify.post('/', { schema }, function (req, reply) {71 t.assert.fail('should not be here')72 reply.code(200).send(req.body.name)73 })74 75 fastify.setErrorHandler(function (error, request, reply) {76 if (error.validation) {77 reply.status(422).send(new Error('validation failed'))78 }79 })80 81 const response = await fastify.inject({82 method: 'POST',83 payload: {84 hello: 'michelangelo'85 },86 url: '/'87 })88 89 t.assert.deepStrictEqual(JSON.parse(response.payload), {90 statusCode: 422,91 error: 'Unprocessable Entity',92 message: 'validation failed'93 })94 t.assert.strictEqual(response.statusCode, 422)95})96 97test('validation error has 400 statusCode set', async (t) => {98 t.plan(2)99 100 const fastify = Fastify()101 102 fastify.setErrorHandler((error, request, reply) => {103 const errorResponse = {104 message: error.message,105 statusCode: error.statusCode || 500106 }107 108 reply.code(errorResponse.statusCode).send(errorResponse)109 })110 111 fastify.post('/', { schema }, echoBody)112 113 const response = await fastify.inject({114 method: 'POST',115 payload: {116 hello: 'michelangelo'117 },118 url: '/'119 })120 121 t.assert.deepStrictEqual(response.json(), {122 statusCode: 400,123 message: "body must have required property 'name'"124 })125 t.assert.strictEqual(response.statusCode, 400)126})127 128test('error inside custom error handler should have validationContext', async (t) => {129 t.plan(1)130 131 const fastify = Fastify()132 133 fastify.post('/', {134 schema,135 validatorCompiler: ({ schema, method, url, httpPart }) => {136 return function (data) {137 return { error: new Error('this failed') }138 }139 }140 }, function (req, reply) {141 t.assert.fail('should not be here')142 reply.code(200).send(req.body.name)143 })144 145 fastify.setErrorHandler(function (error, request, reply) {146 t.assert.strictEqual(error.validationContext, 'body')147 reply.status(500).send(error)148 })149 150 await fastify.inject({151 method: 'POST',152 payload: {153 name: 'michelangelo',154 work: 'artist'155 },156 url: '/'157 })158})159 160test('error inside custom error handler should have validationContext if specified by custom error handler', async (t) => {161 t.plan(1)162 163 const fastify = Fastify()164 165 fastify.post('/', {166 schema,167 validatorCompiler: ({ schema, method, url, httpPart }) => {168 return function (data) {169 const error = new Error('this failed')170 error.validationContext = 'customContext'171 return { error }172 }173 }174 }, function (req, reply) {175 t.assert.fail('should not be here')176 reply.code(200).send(req.body.name)177 })178 179 fastify.setErrorHandler(function (error, request, reply) {180 t.assert.strictEqual(error.validationContext, 'customContext')181 reply.status(500).send(error)182 })183 184 await fastify.inject({185 method: 'POST',186 payload: {187 name: 'michelangelo',188 work: 'artist'189 },190 url: '/'191 })192})193 194test('should be able to attach validation to request', async (t) => {195 t.plan(2)196 197 const fastify = Fastify()198 199 fastify.post('/', { schema, attachValidation: true }, function (req, reply) {200 reply.code(400).send(req.validationError.validation)201 })202 203 const response = await fastify.inject({204 method: 'POST',205 payload: {206 hello: 'michelangelo'207 },208 url: '/'209 })210 211 t.assert.deepStrictEqual(response.json(), [{212 keyword: 'required',213 instancePath: '',214 schemaPath: '#/required',215 params: { missingProperty: 'name' },216 message: 'must have required property \'name\''217 }])218 t.assert.strictEqual(response.statusCode, 400)219})220 221test('should respect when attachValidation is explicitly set to false', async (t) => {222 t.plan(2)223 224 const fastify = Fastify()225 226 fastify.post('/', { schema, attachValidation: false }, function (req, reply) {227 t.assert.fail('should not be here')228 reply.code(200).send(req.validationError.validation)229 })230 231 const response = await fastify.inject({232 method: 'POST',233 payload: {234 hello: 'michelangelo'235 },236 url: '/'237 })238 239 t.assert.deepStrictEqual(JSON.parse(response.payload), {240 statusCode: 400,241 code: 'FST_ERR_VALIDATION',242 error: 'Bad Request',243 message: "body must have required property 'name'"244 })245 t.assert.strictEqual(response.statusCode, 400)246})247 248test('Attached validation error should take precedence over setErrorHandler', async (t) => {249 t.plan(2)250 251 const fastify = Fastify()252 253 fastify.post('/', { schema, attachValidation: true }, function (req, reply) {254 reply.code(400).send('Attached: ' + req.validationError)255 })256 257 fastify.setErrorHandler(function (error, request, reply) {258 t.assert.fail('should not be here')259 if (error.validation) {260 reply.status(422).send(new Error('validation failed'))261 }262 })263 264 const response = await fastify.inject({265 method: 'POST',266 payload: {267 hello: 'michelangelo'268 },269 url: '/'270 })271 272 t.assert.deepStrictEqual(response.payload, "Attached: Error: body must have required property 'name'")273 t.assert.strictEqual(response.statusCode, 400)274})275 276test('should handle response validation error', async (t) => {277 t.plan(2)278 279 const response = {280 200: {281 type: 'object',282 required: ['name', 'work'],283 properties: {284 name: { type: 'string' },285 work: { type: 'string' }286 }287 }288 }289 290 const fastify = Fastify()291 292 fastify.get('/', { schema: { response } }, function (req, reply) {293 try {294 reply.code(200).send({ work: 'actor' })295 } catch (error) {296 reply.code(500).send(error)297 }298 })299 300 const injectResponse = await fastify.inject({301 method: 'GET',302 payload: { },303 url: '/'304 })305 306 t.assert.strictEqual(injectResponse.statusCode, 500)307 t.assert.strictEqual(injectResponse.payload, '{"statusCode":500,"error":"Internal Server Error","message":"\\"name\\" is required!"}')308})309 310test('should handle response validation error with promises', async (t) => {311 t.plan(2)312 313 const response = {314 200: {315 type: 'object',316 required: ['name', 'work'],317 properties: {318 name: { type: 'string' },319 work: { type: 'string' }320 }321 }322 }323 324 const fastify = Fastify()325 326 fastify.get('/', { schema: { response } }, function (req, reply) {327 return Promise.resolve({ work: 'actor' })328 })329 330 const injectResponse = await fastify.inject({331 method: 'GET',332 payload: { },333 url: '/'334 })335 336 t.assert.strictEqual(injectResponse.statusCode, 500)337 t.assert.strictEqual(injectResponse.payload, '{"statusCode":500,"error":"Internal Server Error","message":"\\"name\\" is required!"}')338})339 340test('should return a defined output message parsing AJV errors', async (t) => {341 t.plan(2)342 343 const body = {344 type: 'object',345 required: ['name', 'work'],346 properties: {347 name: { type: 'string' },348 work: { type: 'string' }349 }350 }351 352 const fastify = Fastify()353 354 fastify.post('/', { schema: { body } }, function (req, reply) {355 t.assert.fail()356 })357 358 const response = await fastify.inject({359 method: 'POST',360 payload: { },361 url: '/'362 })363 364 t.assert.strictEqual(response.statusCode, 400)365 t.assert.strictEqual(response.payload, '{"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body must have required property \'name\'"}')366})367 368test('should return a defined output message parsing JOI errors', async (t) => {369 t.plan(2)370 371 const body = Joi.object().keys({372 name: Joi.string().required(),373 work: Joi.string().required()374 }).required()375 376 const fastify = Fastify()377 378 fastify.post('/', {379 schema: { body },380 validatorCompiler: ({ schema, method, url, httpPart }) => {381 return data => schema.validate(data)382 }383 },384 function (req, reply) {385 t.assert.fail()386 })387 388 const response = await fastify.inject({389 method: 'POST',390 payload: {},391 url: '/'392 })393 394 t.assert.strictEqual(response.statusCode, 400)395 t.assert.strictEqual(response.payload, '{"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"\\"name\\" is required"}')396})397 398test('should return a defined output message parsing JOI error details', async (t) => {399 t.plan(2)400 401 const body = Joi.object().keys({402 name: Joi.string().required(),403 work: Joi.string().required()404 }).required()405 406 const fastify = Fastify()407 408 fastify.post('/', {409 schema: { body },410 validatorCompiler: ({ schema, method, url, httpPart }) => {411 return data => {412 const validation = schema.validate(data)413 return { error: validation.error.details }414 }415 }416 },417 function (req, reply) {418 t.assert.fail()419 })420 421 const response = await fastify.inject({422 method: 'POST',423 payload: {},424 url: '/'425 })426 427 t.assert.strictEqual(response.statusCode, 400)428 t.assert.strictEqual(response.payload, '{"statusCode":400,"code":"FST_ERR_VALIDATION","error":"Bad Request","message":"body \\"name\\" is required"}')429})430 431test('the custom error formatter context must be the server instance', async (t) => {432 t.plan(3)433 434 const fastify = Fastify()435 436 fastify.setSchemaErrorFormatter(function (errors, dataVar) {437 t.assert.deepStrictEqual(this, fastify)438 return new Error('my error')439 })440 441 fastify.post('/', { schema }, echoBody)442 443 const response = await fastify.inject({444 method: 'POST',445 payload: {446 hello: 'michelangelo'447 },448 url: '/'449 })450 451 t.assert.deepStrictEqual(response.json(), {452 statusCode: 400,453 code: 'FST_ERR_VALIDATION',454 error: 'Bad Request',455 message: 'my error'456 })457 t.assert.strictEqual(response.statusCode, 400)458})459 460test('the custom error formatter context must be the server instance in options', async (t) => {461 t.plan(3)462 463 const fastify = Fastify({464 schemaErrorFormatter: function (errors, dataVar) {465 t.assert.deepStrictEqual(this, fastify)466 return new Error('my error')467 }468 })469 470 fastify.post('/', { schema }, echoBody)471 472 const response = await fastify.inject({473 method: 'POST',474 payload: {475 hello: 'michelangelo'476 },477 url: '/'478 })479 480 t.assert.deepStrictEqual(response.json(), {481 statusCode: 400,482 code: 'FST_ERR_VALIDATION',483 error: 'Bad Request',484 message: 'my error'485 })486 t.assert.strictEqual(response.statusCode, 400)487})488 489test('should call custom error formatter', async (t) => {490 t.plan(8)491 492 const fastify = Fastify({493 schemaErrorFormatter: (errors, dataVar) => {494 t.assert.strictEqual(errors.length, 1)495 t.assert.strictEqual(errors[0].message, "must have required property 'name'")496 t.assert.strictEqual(errors[0].keyword, 'required')497 t.assert.strictEqual(errors[0].schemaPath, '#/required')498 t.assert.deepStrictEqual(errors[0].params, {499 missingProperty: 'name'500 })501 t.assert.strictEqual(dataVar, 'body')502 return new Error('my error')503 }504 })505 506 fastify.post('/', { schema }, echoBody)507 508 const response = await fastify.inject({509 method: 'POST',510 payload: {511 hello: 'michelangelo'512 },513 url: '/'514 })515 516 t.assert.deepStrictEqual(response.json(), {517 statusCode: 400,518 code: 'FST_ERR_VALIDATION',519 error: 'Bad Request',520 message: 'my error'521 })522 t.assert.strictEqual(response.statusCode, 400)523})524 525test('should catch error inside formatter and return message', async (t) => {526 t.plan(2)527 528 const fastify = Fastify({529 schemaErrorFormatter: (errors, dataVar) => {530 throw new Error('abc')531 }532 })533 534 fastify.post('/', { schema }, echoBody)535 536 const response = await fastify.inject({537 method: 'POST',538 payload: {539 hello: 'michelangelo'540 },541 url: '/'542 })543 544 t.assert.deepStrictEqual(response.json(), {545 statusCode: 500,546 error: 'Internal Server Error',547 message: 'abc'548 })549 t.assert.strictEqual(response.statusCode, 500)550})551 552test('cannot create a fastify instance with wrong type of errorFormatter', async (t) => {553 t.plan(3)554 555 try {556 Fastify({557 schemaErrorFormatter: async (errors, dataVar) => {558 return new Error('should not execute')559 }560 })561 } catch (err) {562 t.assert.strictEqual(err.code, 'FST_ERR_SCHEMA_ERROR_FORMATTER_NOT_FN')563 }564 565 try {566 Fastify({567 schemaErrorFormatter: 500568 })569 } catch (err) {570 t.assert.strictEqual(err.code, 'FST_ERR_SCHEMA_ERROR_FORMATTER_NOT_FN')571 }572 573 try {574 const fastify = Fastify()575 fastify.setSchemaErrorFormatter(500)576 } catch (err) {577 t.assert.strictEqual(err.code, 'FST_ERR_SCHEMA_ERROR_FORMATTER_NOT_FN')578 }579})580 581test('should register a route based schema error formatter', async (t) => {582 t.plan(2)583 584 const fastify = Fastify()585 586 fastify.post('/', {587 schema,588 schemaErrorFormatter: (errors, dataVar) => {589 return new Error('abc')590 }591 }, echoBody)592 593 const response = await fastify.inject({594 method: 'POST',595 payload: {596 hello: 'michelangelo'597 },598 url: '/'599 })600 601 t.assert.deepStrictEqual(response.json(), {602 statusCode: 400,603 code: 'FST_ERR_VALIDATION',604 error: 'Bad Request',605 message: 'abc'606 })607 t.assert.strictEqual(response.statusCode, 400)608})609 610test('prefer route based error formatter over global one', async (t) => {611 t.plan(6)612 613 const fastify = Fastify({614 schemaErrorFormatter: (errors, dataVar) => {615 return new Error('abc123')616 }617 })618 619 fastify.post('/', {620 schema,621 schemaErrorFormatter: (errors, dataVar) => {622 return new Error('123')623 }624 }, echoBody)625 626 fastify.post('/abc', {627 schema,628 schemaErrorFormatter: (errors, dataVar) => {629 return new Error('abc')630 }631 }, echoBody)632 633 fastify.post('/test', { schema }, echoBody)634 635 const response1 = await fastify.inject({636 method: 'POST',637 payload: {638 hello: 'michelangelo'639 },640 url: '/'641 })642 643 t.assert.deepStrictEqual(response1.json(), {644 statusCode: 400,645 code: 'FST_ERR_VALIDATION',646 error: 'Bad Request',647 message: '123'648 })649 t.assert.strictEqual(response1.statusCode, 400)650 651 const response2 = await fastify.inject({652 method: 'POST',653 payload: {654 hello: 'michelangelo'655 },656 url: '/abc'657 })658 659 t.assert.deepStrictEqual(response2.json(), {660 statusCode: 400,661 code: 'FST_ERR_VALIDATION',662 error: 'Bad Request',663 message: 'abc'664 })665 t.assert.strictEqual(response2.statusCode, 400)666 667 const response3 = await fastify.inject({668 method: 'POST',669 payload: {670 hello: 'michelangelo'671 },672 url: '/test'673 })674 675 t.assert.deepStrictEqual(response3.json(), {676 statusCode: 400,677 code: 'FST_ERR_VALIDATION',678 error: 'Bad Request',679 message: 'abc123'680 })681 t.assert.strictEqual(response3.statusCode, 400)682})683 684test('adding schemaErrorFormatter', async (t) => {685 t.plan(2)686 687 const fastify = Fastify()688 689 fastify.setSchemaErrorFormatter((errors, dataVar) => {690 return new Error('abc')691 })692 693 fastify.post('/', { schema }, echoBody)694 695 const response = await fastify.inject({696 method: 'POST',697 payload: {698 hello: 'michelangelo'699 },700 url: '/'701 })702 703 t.assert.deepStrictEqual(response.json(), {704 statusCode: 400,705 code: 'FST_ERR_VALIDATION',706 error: 'Bad Request',707 message: 'abc'708 })709 t.assert.strictEqual(response.statusCode, 400)710})711 712test('plugin override', async (t) => {713 t.plan(10)714 715 const fastify = Fastify({716 schemaErrorFormatter: (errors, dataVar) => {717 return new Error('B')718 }719 })720 721 fastify.register((instance, opts, done) => {722 instance.setSchemaErrorFormatter((errors, dataVar) => {723 return new Error('C')724 })725 726 instance.post('/d', {727 schema,728 schemaErrorFormatter: (errors, dataVar) => {729 return new Error('D')730 }731 }, function (req, reply) {732 reply.code(200).send(req.body.name)733 })734 735 instance.post('/c', { schema }, echoBody)736 737 instance.register((subinstance, opts, done) => {738 subinstance.post('/stillC', { schema }, echoBody)739 done()740 })741 742 done()743 })744 745 fastify.post('/b', { schema }, echoBody)746 747 fastify.post('/', {748 schema,749 schemaErrorFormatter: (errors, dataVar) => {750 return new Error('A')751 }752 }, echoBody)753 754 const response1 = await fastify.inject({755 method: 'POST',756 payload: {757 hello: 'michelangelo'758 },759 url: '/'760 })761 762 t.assert.deepStrictEqual(response1.json(), {763 statusCode: 400,764 code: 'FST_ERR_VALIDATION',765 error: 'Bad Request',766 message: 'A'767 })768 t.assert.strictEqual(response1.statusCode, 400)769 770 const response2 = await fastify.inject({771 method: 'POST',772 payload: {773 hello: 'michelangelo'774 },775 url: '/b'776 })777 778 t.assert.deepStrictEqual(response2.json(), {779 statusCode: 400,780 code: 'FST_ERR_VALIDATION',781 error: 'Bad Request',782 message: 'B'783 })784 t.assert.strictEqual(response2.statusCode, 400)785 786 const response3 = await fastify.inject({787 method: 'POST',788 payload: {789 hello: 'michelangelo'790 },791 url: '/c'792 })793 794 t.assert.deepStrictEqual(response3.json(), {795 statusCode: 400,796 code: 'FST_ERR_VALIDATION',797 error: 'Bad Request',798 message: 'C'799 })800 t.assert.strictEqual(response3.statusCode, 400)801 802 const response4 = await fastify.inject({803 method: 'POST',804 payload: {805 hello: 'michelangelo'806 },807 url: '/d'808 })809 810 t.assert.deepStrictEqual(response4.json(), {811 statusCode: 400,812 code: 'FST_ERR_VALIDATION',813 error: 'Bad Request',814 message: 'D'815 })816 t.assert.strictEqual(response4.statusCode, 400)817 818 const response5 = await fastify.inject({819 method: 'POST',820 payload: {821 hello: 'michelangelo'822 },823 url: '/stillC'824 })825 826 t.assert.deepStrictEqual(response5.json(), {827 statusCode: 400,828 code: 'FST_ERR_VALIDATION',829 error: 'Bad Request',830 message: 'C'831 })832 t.assert.strictEqual(response5.statusCode, 400)833})834 