diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index f42e7d1..0000000 --- a/jest.config.js +++ /dev/null @@ -1,20 +0,0 @@ -export default { - testEnvironment: 'node', - collectCoverage: true, - collectCoverageFrom: [ - '/lib/**/*.js', - '!/lib/index.js', - ], - coverageDirectory: '/coverage', - coverageThreshold: { - global: { - branches: 100, - functions: 100, - lines: 100, - statements: 100, - }, - }, - // Workaround for jest --watch EMFILE on macOS without watchman. - // See https://github.com/jestjs/jest/issues/8088 - watchPathIgnorePatterns: ['/node_modules/'], -}; diff --git a/package.json b/package.json index 6ad79ef..55df436 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ }, "scripts": { "test": "is-ci-cli test:ci test:local", - "test:local": "node --experimental-vm-modules node_modules/.bin/jest --watch --verbose", - "test:ci": "node --experimental-vm-modules node_modules/.bin/jest --ci", + "test:local": "node --test --watch test/*.test.js", + "test:ci": "c8 --reporter=lcov --reporter=text --100 node --test test/*.test.js", "benchmark": "node utils/benchmark", "toc": "node utils/generate-toc", "version": "npm run toc && git add README.md" @@ -49,13 +49,14 @@ "@hapi/teamwork": "5.x.x", "@types/express": "5.x.x", "artificial": "1.x.x", + "c8": "10.x.x", "cookie-parser": "1.x.x", "cookie-signature": "1.x.x", "eslint": "9.x.x", + "expect": "30.x.x", "express": "5.x.x", "globals": "16.x.x", "is-ci-cli": "2.x.x", - "jest": "30.x.x", "markdown-toc": "1.x.x", "neostandard": "0.x.x", "tinybench": "5.x.x" diff --git a/test/__snapshots__/celebrate.test.js.snap b/test/__snapshots__/celebrate.test.js.snap deleted file mode 100644 index 6402a3b..0000000 --- a/test/__snapshots__/celebrate.test.js.snap +++ /dev/null @@ -1,61 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`celebrate() validates the entire request (params, query, body) with full validatate mode 1`] = ` -Map { - "params" => [ValidationError: "id" must be a string], - "query" => [ValidationError: "end" is not allowed], - "body" => [ValidationError: "role" must be a number], -} -`; - -exports[`errors() honors the configuration options 1`] = ` -{ - "error": "Conflict", - "message": "your request is bad and you should feel bad", - "statusCode": 409, - "validation": { - "query": { - "keys": [ - "role", - ], - "message": ""role" must be greater than or equal to 4", - "source": "query", - }, - }, -} -`; - -exports[`errors() includes more information when abourtEarly is false 1`] = ` -{ - "error": "Bad Request", - "message": "Validation failed", - "statusCode": 400, - "validation": { - "query": { - "keys": [ - "role", - "name", - ], - "message": ""role" must be greater than or equal to 4. "name" is required", - "source": "query", - }, - }, -} -`; - -exports[`errors() responds with a joi error from celebrate middleware 1`] = ` -{ - "error": "Bad Request", - "message": "Validation failed", - "statusCode": 400, - "validation": { - "query": { - "keys": [ - "role", - ], - "message": ""role" must be greater than or equal to 4", - "source": "query", - }, - }, -} -`; diff --git a/test/celebrate.test.js b/test/celebrate.test.js index 78ea725..b385e7f 100644 --- a/test/celebrate.test.js +++ b/test/celebrate.test.js @@ -1,4 +1,5 @@ -import { jest } from '@jest/globals'; +import { describe, it, mock } from 'node:test'; +import { expect } from 'expect'; import { faker } from '@faker-js/faker'; import { celebrate, @@ -12,46 +13,48 @@ import { } from '../lib/index.js'; describe('celebrate()', () => { - describe.each` - schema - ${false} - ${undefined} - ${{}} - ${{ [Segments.QUERY]: { name: Joi.string(), age: Joi.number() }, foo: Joi.string() }} - `('celebrate($schema)', ({ schema }) => { - it('throws an error', () => { - expect(() => { - celebrate(schema); - }).toThrow(Joi.ValidationError); - }); - }); - - describe.each` - segment | schema | req | message - ${Segments.HEADERS} | ${{ [Segments.HEADERS]: { accept: Joi.string().regex(/xml/) } }} | ${{ [Segments.HEADERS]: { accept: 'application/json' } }} | ${'"accept" with value "application/json" fails to match the required pattern: /xml/'} - ${Segments.PARAMS} | ${{ [Segments.PARAMS]: { id: Joi.string().token() } }} | ${{ [Segments.PARAMS]: { id: '@@@' } }} | ${'"id" must only contain alpha-numeric and underscore characters'} - ${Segments.QUERY} | ${{ [Segments.QUERY]: Joi.object().keys({ start: Joi.date() }) }} | ${{ [Segments.QUERY]: { end: faker.number.int() } }} | ${'"end" is not allowed'} - ${Segments.BODY} | ${{ [Segments.BODY]: { first: Joi.string().required(), last: Joi.string(), role: Joi.number().integer() } }} | ${{ [Segments.BODY]: { first: faker.person.firstName(), last: faker.number.int() }, method: 'POST' }} | ${'"last" must be a string'} - ${Segments.COOKIES} | ${{ [Segments.COOKIES]: { state: Joi.string().required() } }} | ${{ [Segments.COOKIES]: { state: faker.number.int() } }} | ${'"state" must be a string'} - ${Segments.SIGNEDCOOKIES} | ${{ [Segments.SIGNEDCOOKIES]: { uid: Joi.string().required() } }} | ${{ [Segments.SIGNEDCOOKIES]: { uid: faker.number.int() } }} | ${'"uid" must be a string'} - `('celebate middleware', ({ - schema, req, message, segment, - }) => { - describe.each` - fn | kind - ${celebrate} | ${'celebrate'} - ${celebrator(undefined, undefined)} | ${'celebrator'} - `('', ({ fn, kind }) => { - it(`validates ${segment} correctly with ${kind}`, () => { - expect.assertions(2); - const middleware = fn(schema); - - return middleware(req, null, (err) => { - expect(isCelebrateError(err)).toBe(true); - expect(err.details.get(segment).message).toBe(message); - }); + const invalidSchemaCases = [ + { schema: false }, + { schema: undefined }, + { schema: {} }, + { schema: { [Segments.QUERY]: { name: Joi.string(), age: Joi.number() }, foo: Joi.string() } }, + ]; + for (const { schema } of invalidSchemaCases) { + describe(`celebrate(${String(schema)})`, () => { + it('throws an error', () => { + expect(() => { + celebrate(schema); + }).toThrow(Joi.ValidationError); }); }); + } + + const segmentCases = [ + { segment: Segments.HEADERS, schema: { [Segments.HEADERS]: { accept: Joi.string().regex(/xml/) } }, req: { [Segments.HEADERS]: { accept: 'application/json' } }, message: '"accept" with value "application/json" fails to match the required pattern: /xml/' }, + { segment: Segments.PARAMS, schema: { [Segments.PARAMS]: { id: Joi.string().token() } }, req: { [Segments.PARAMS]: { id: '@@@' } }, message: '"id" must only contain alpha-numeric and underscore characters' }, + { segment: Segments.QUERY, schema: { [Segments.QUERY]: Joi.object().keys({ start: Joi.date() }) }, req: { [Segments.QUERY]: { end: faker.number.int() } }, message: '"end" is not allowed' }, + { segment: Segments.BODY, schema: { [Segments.BODY]: { first: Joi.string().required(), last: Joi.string(), role: Joi.number().integer() } }, req: { [Segments.BODY]: { first: faker.person.firstName(), last: faker.number.int() }, method: 'POST' }, message: '"last" must be a string' }, + { segment: Segments.COOKIES, schema: { [Segments.COOKIES]: { state: Joi.string().required() } }, req: { [Segments.COOKIES]: { state: faker.number.int() } }, message: '"state" must be a string' }, + { segment: Segments.SIGNEDCOOKIES, schema: { [Segments.SIGNEDCOOKIES]: { uid: Joi.string().required() } }, req: { [Segments.SIGNEDCOOKIES]: { uid: faker.number.int() } }, message: '"uid" must be a string' }, + ]; + const fnCases = [ + { fn: celebrate, kind: 'celebrate' }, + { fn: celebrator(undefined, undefined), kind: 'celebrator' }, + ]; + describe('celebate middleware', () => { + for (const { schema, req, message, segment } of segmentCases) { + for (const { fn, kind } of fnCases) { + it(`validates ${segment} correctly with ${kind}`, () => { + expect.assertions(2); + const middleware = fn(schema); + + return middleware(req, null, (err) => { + expect(isCelebrateError(err)).toBe(true); + expect(err.details.get(segment).message).toBe(message); + }); + }); + } + } }); it('errors on the first validation problem (params, query, body) by default', () => { @@ -88,7 +91,7 @@ describe('celebrate()', () => { }); it('validates the entire request (params, query, body) with full validatate mode', () => { - expect.assertions(2); + expect.assertions(5); const middleware = celebrate({ [Segments.PARAMS]: { id: Joi.string().required(), @@ -120,7 +123,10 @@ describe('celebrate()', () => { method: 'POST', }, null, (err) => { expect(isCelebrateError(err)).toBe(true); - expect(err.details).toMatchSnapshot(); + expect([...err.details.keys()]).toEqual([Segments.PARAMS, Segments.QUERY, Segments.BODY]); + expect(err.details.get(Segments.PARAMS).message).toBe('"id" must be a string'); + expect(err.details.get(Segments.QUERY).message).toBe('"end" is not allowed'); + expect(err.details.get(Segments.BODY).message).toBe('"role" must be a number'); }); }); @@ -409,14 +415,25 @@ describe('errors()', () => { }, }); const handler = errors(); - const next = jest.fn(); + const next = mock.fn(); const res = { status (statusCode) { expect(statusCode).toBe(400); return { send (err) { - expect(err).toMatchSnapshot(); - expect(next).not.toHaveBeenCalled(); + expect(err).toEqual({ + error: 'Bad Request', + message: 'Validation failed', + statusCode: 400, + validation: { + query: { + keys: ['role'], + message: '"role" must be greater than or equal to 4', + source: 'query', + }, + }, + }); + expect(next.mock.callCount()).toBe(0); }, }; }, @@ -464,14 +481,25 @@ describe('errors()', () => { abortEarly: false, }); const handler = errors(); - const next = jest.fn(); + const next = mock.fn(); const res = { status (statusCode) { expect(statusCode).toBe(400); return { send (err) { - expect(err).toMatchSnapshot(); - expect(next).not.toHaveBeenCalled(); + expect(err).toEqual({ + error: 'Bad Request', + message: 'Validation failed', + statusCode: 400, + validation: { + query: { + keys: ['role', 'name'], + message: '"role" must be greater than or equal to 4. "name" is required', + source: 'query', + }, + }, + }); + expect(next.mock.callCount()).toBe(0); }, }; }, @@ -497,7 +525,7 @@ describe('errors()', () => { const statusCode = 409; const message = 'your request is bad and you should feel bad'; const handler = errors({ statusCode, message }); - const next = jest.fn(); + const next = mock.fn(); const res = { status (code) { expect(code).toBe(statusCode); @@ -505,8 +533,19 @@ describe('errors()', () => { send (err) { expect(err).toHaveProperty('statusCode', statusCode); expect(err).toHaveProperty('message', message); - expect(err).toMatchSnapshot(); - expect(next).not.toHaveBeenCalled(); + expect(err).toEqual({ + error: 'Conflict', + message, + statusCode, + validation: { + query: { + keys: ['role'], + message: '"role" must be greater than or equal to 4', + source: 'query', + }, + }, + }); + expect(next.mock.callCount()).toBe(0); }, }; }, @@ -529,20 +568,22 @@ describe('errors()', () => { }); describe('isCelebrateError()', () => { - describe.each` - value | expected - ${Error()} | ${false} - ${'errr'} | ${false} - ${0} | ${false} - ${[0, 1]} | ${false} - ${null} | ${false} - ${undefined} | ${false} - `('isCelebrateError($value)', ({ value, expected }) => { - it(`returns ${expected}`, () => { - expect.assertions(1); - expect(isCelebrateError(value)).toBe(expected); + const isCelebrateErrorCases = [ + { value: Error(), expected: false }, + { value: 'errr', expected: false }, + { value: 0, expected: false }, + { value: [0, 1], expected: false }, + { value: null, expected: false }, + { value: undefined, expected: false }, + ]; + for (const { value, expected } of isCelebrateErrorCases) { + describe(`isCelebrateError(${String(value)})`, () => { + it(`returns ${expected}`, () => { + expect.assertions(1); + expect(isCelebrateError(value)).toBe(expected); + }); }); - }); + } it('returns true if the error object came from celebrate', () => { expect.assertions(1); diff --git a/test/integration.test.js b/test/integration.test.js index dd3ffd1..c0b79f6 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,4 +1,5 @@ -import { jest } from '@jest/globals'; +import { describe, test, mock } from 'node:test'; +import { expect } from 'expect'; import Express from 'express'; import Artificial from 'artificial'; import signature from 'cookie-signature'; @@ -52,7 +53,7 @@ describe('validations', () => { expect.assertions(4); const server = Server(); const team = new Teamwork.Team(); - const next = jest.fn(); + const next = mock.fn(); server.get('/', celebrate({ [Segments.HEADERS]: { @@ -75,14 +76,14 @@ describe('validations', () => { expect(statusCode).toBe(400); expect(isCelebrateError(server.errors.error)).toBe(true); expect(server.errors.error.details.has(Segments.HEADERS)).toBe(true); - expect(next).not.toHaveBeenCalled(); + expect(next.mock.callCount()).toBe(0); }); test('req.params', async () => { expect.assertions(4); const server = Server(); const team = new Teamwork.Team(); - const next = jest.fn(); + const next = mock.fn(); server.get('/user/:id', celebrate({ [Segments.PARAMS]: { @@ -100,14 +101,14 @@ describe('validations', () => { expect(statusCode).toBe(400); expect(isCelebrateError(server.errors.error)).toBe(true); expect(server.errors.error.details.has(Segments.PARAMS)).toBe(true); - expect(next).not.toHaveBeenCalled(); + expect(next.mock.callCount()).toBe(0); }); test('req.query', async () => { expect.assertions(4); const server = Server(); const team = new Teamwork.Team(); - const next = jest.fn(); + const next = mock.fn(); server.get('/', celebrate({ [Segments.QUERY]: Joi.object().keys({ @@ -124,14 +125,14 @@ describe('validations', () => { expect(statusCode).toBe(400); expect(isCelebrateError(server.errors.error)).toBe(true); expect(server.errors.error.details.has(Segments.QUERY)).toBe(true); - expect(next).not.toHaveBeenCalled(); + expect(next.mock.callCount()).toBe(0); }); test('req.cookies', async () => { expect.assertions(4); const server = Server(); const team = new Teamwork.Team(); - const next = jest.fn(); + const next = mock.fn(); server.post('/', celebrate({ cookies: { @@ -152,14 +153,14 @@ describe('validations', () => { expect(statusCode).toBe(400); expect(isCelebrateError(server.errors.error)).toBe(true); expect(server.errors.error.details.has(Segments.COOKIES)).toBe(true); - expect(next).not.toHaveBeenCalled(); + expect(next.mock.callCount()).toBe(0); }); test('req.signedCookies', async () => { expect.assertions(4); const server = Server(); const team = new Teamwork.Team(); - const next = jest.fn(); + const next = mock.fn(); server.get('/', celebrate({ [Segments.SIGNEDCOOKIES]: { @@ -182,14 +183,14 @@ describe('validations', () => { expect(statusCode).toBe(400); expect(isCelebrateError(server.errors.error)).toBe(true); expect(server.errors.error.details.has(Segments.SIGNEDCOOKIES)).toBe(true); - expect(next).not.toHaveBeenCalled(); + expect(next.mock.callCount()).toBe(0); }); test('req.body', async () => { expect.assertions(4); const server = Server(); const team = new Teamwork.Team(); - const next = jest.fn(); + const next = mock.fn(); server.post('/', celebrate({ [Segments.BODY]: { @@ -213,7 +214,7 @@ describe('validations', () => { expect(statusCode).toBe(400); expect(isCelebrateError(server.errors.error)).toBe(true); expect(server.errors.error.details.has(Segments.BODY)).toBe(true); - expect(next).not.toHaveBeenCalled(); + expect(next.mock.callCount()).toBe(0); }); }); @@ -365,7 +366,7 @@ describe('reqContext', () => { expect.assertions(4); const server = Server(); const team = new Teamwork.Team(); - const next = jest.fn(); + const next = mock.fn(); server.post('/:userId', celebrate({ [Segments.BODY]: { @@ -391,7 +392,7 @@ describe('reqContext', () => { expect(res.statusCode).toBe(400); expect(isCelebrateError(server.errors.error)).toBe(true); expect(server.errors.error.details.has(Segments.BODY)).toBe(true); - expect(next).not.toHaveBeenCalled(); + expect(next.mock.callCount()).toBe(0); }); });