diff --git a/.github/workflows/ci-module.yml b/.github/workflows/ci-module.yml index 44369c8..49bcabb 100644 --- a/.github/workflows/ci-module.yml +++ b/.github/workflows/ci-module.yml @@ -1,13 +1,13 @@ name: ci on: - push: - branches: - - master - - next - pull_request: - workflow_dispatch: + push: + branches: + - master + - next + pull_request: + workflow_dispatch: jobs: - test: - uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-18-hapi-21 + test: + uses: hapijs/.github/.github/workflows/ci-module.yml@min-node-22-hapi-21 diff --git a/.gitignore b/.gitignore index 8f679c9..af5a347 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ **/node_modules **/package-lock.json -coverage.* +coverage/ **/.DS_Store **/._* diff --git a/API.md b/API.md index b0c0917..419f5fb 100755 --- a/API.md +++ b/API.md @@ -1,6 +1,6 @@ - **boom** provides a set of utilities for returning HTTP errors. Each utility returns a `Boom` error response object which includes the following properties: + - `message` - the error message. - `output` - the formatted response. Can be directly manipulated after object construction to return a custom error response. Allowed root keys: @@ -16,9 +16,10 @@ error response object which includes the following properties: - optional `cause` - the error cause, as set by constructor. The object has additional properties from the `Boom` prototype: + - `name` - string with error name. Set to `'Boom'`. - `isBoom` - set to `true`, indicating this is a `Boom` object instance. Note that this boolean should - only be tested if the error is an instance of `Error`. If it is not certain, use [`Boom.isBoom()`](#isboomerr-statuscode) instead. + only be tested if the error is an instance of `Error`. If it is not certain, use [`Boom.isBoom()`](#isboomerr-statuscode) instead. - `isServer` - convenience boolean indicating status code >= 500. The object also supports the following method: @@ -35,10 +36,11 @@ Rebuilds `error.output` using the other object properties where: ##### `new Boom([message], [options])` Creates a new `Boom` sub-classed `Error` object, where: + - `message` - the error message. - `options` - and optional object where: - - `statusCode` - the HTTP status code. Defaults to `500`. - - `cause` - the error that caused the boom error. + - `statusCode` - the HTTP status code. Defaults to `500`. + - `cause` - the error that caused the boom error. - `data` - additional error information, assigned to `this.data`. - `headers` - an object containing any HTTP headers where each key is a header name and value is the header content. - `ctor` - constructor reference used to crop the exception call stack output. @@ -49,13 +51,14 @@ Creates a new `Boom` sub-classed `Error` object, where: Creates a `Boom` object similar to [`new Boom()`](#new-boommessage-options), except it applies the `options` to the existing error when it is a `Boom` object, where: + - `err` - the object to boomify, set as `cause` when `err` is not a `Boom` object. - `options` - optional object with the following optional settings: - - `statusCode` - the HTTP status code. Defaults to `500` if no status code is already set and `err` is not a `Boom` object. - - `message` - error message string. If the error already has a message, the provided `message` is added as a prefix. - - `override` - if `false`, the `err` provided is a `Boom` object, and a `statusCode` or `message` are provided, - the values are ignored. Defaults to `true` (apply the provided `statusCode` and `message` options to the error - regardless of its type). + - `statusCode` - the HTTP status code. Defaults to `500` if no status code is already set and `err` is not a `Boom` object. + - `message` - error message string. If the error already has a message, the provided `message` is added as a prefix. + - `override` - if `false`, the `err` provided is a `Boom` object, and a `statusCode` or `message` are provided, + the values are ignored. Defaults to `true` (apply the provided `statusCode` and `message` options to the error + regardless of its type). - it returns a `Boom` object with the boomified error Note that [`new Boom()`](#new-boommessage-options) should generally be preferred in cases where the error can come from awaited logic, or has been passed around. @@ -68,6 +71,7 @@ const boomified = Boom.boomify(error, { statusCode: 400 }); ##### `isBoom(err, [statusCode])` Identifies whether an error is a `Boom` object. Same as calling `err instanceof Boom.Boom`. + - `err` - Error object. - `statusCode` - optional status code. @@ -81,6 +85,7 @@ Boom.isBoom(Boom.badRequest(), 400); // true ##### `Boom.badRequest([message], [data])` Returns a 400 Bad Request error where: + - `message` - optional message. - `data` - optional additional error data. @@ -101,14 +106,15 @@ Generates the following response payload: ##### `Boom.unauthorized([message], [scheme], [attributes])` Returns a 401 Unauthorized error where: + - `message` - optional message. - `scheme` can be one of the following: - - an authentication scheme name - - an array of string values. These values will be separated by ', ' and set to the 'WWW-Authenticate' header. + - an authentication scheme name + - an array of string values. These values will be separated by ', ' and set to the 'WWW-Authenticate' header. - `attributes` - an object of values to use while setting the 'WWW-Authenticate' header. This value is only used when `scheme` is a string, otherwise it is ignored. Every key/value pair will be included in the 'WWW-Authenticate' in the format of 'key="value"'. Alternatively value can be a string which is used to set the - value of the scheme, for example setting the token value for negotiate header. If string is used message parameter must be null. + value of the scheme, for example setting the token value for negotiate header. If string is used message parameter must be null. `null` and `undefined` will be replaced with an empty string. If `attributes` is set, `message` will be used as the 'error' segment of the 'WWW-Authenticate' header. If `message` is unset, the 'error' segment of the header will not be present and `isMissing` will be true on the error object. @@ -184,6 +190,7 @@ Generates the following response: ##### `Boom.paymentRequired([message], [data])` Returns a 402 Payment Required error where: + - `message` - optional message. - `data` - optional additional error data. @@ -204,6 +211,7 @@ Generates the following response payload: ##### `Boom.forbidden([message], [data])` Returns a 403 Forbidden error where: + - `message` - optional message. - `data` - optional additional error data. @@ -224,6 +232,7 @@ Generates the following response payload: ##### `Boom.notFound([message], [data])` Returns a 404 Not Found error where: + - `message` - optional message. - `data` - optional additional error data. @@ -244,6 +253,7 @@ Generates the following response payload: ##### `Boom.methodNotAllowed([message], [data], [allow])` Returns a 405 Method Not Allowed error where: + - `message` - optional message. - `data` - optional additional error data. - `allow` - optional string or array of strings (to be combined and separated by ', ') which is set to the 'Allow' header. @@ -265,6 +275,7 @@ Generates the following response payload: ##### `Boom.notAcceptable([message], [data])` Returns a 406 Not Acceptable error where: + - `message` - optional message. - `data` - optional additional error data. @@ -285,6 +296,7 @@ Generates the following response payload: ##### `Boom.proxyAuthRequired([message], [data])` Returns a 407 Proxy Authentication Required error where: + - `message` - optional message. - `data` - optional additional error data. @@ -305,6 +317,7 @@ Generates the following response payload: ##### `Boom.clientTimeout([message], [data])` Returns a 408 Request Time-out error where: + - `message` - optional message. - `data` - optional additional error data. @@ -325,6 +338,7 @@ Generates the following response payload: ##### `Boom.conflict([message], [data])` Returns a 409 Conflict error where: + - `message` - optional message. - `data` - optional additional error data. @@ -345,6 +359,7 @@ Generates the following response payload: ##### `Boom.resourceGone([message], [data])` Returns a 410 Gone error where: + - `message` - optional message. - `data` - optional additional error data. @@ -365,6 +380,7 @@ Generates the following response payload: ##### `Boom.lengthRequired([message], [data])` Returns a 411 Length Required error where: + - `message` - optional message. - `data` - optional additional error data. @@ -385,6 +401,7 @@ Generates the following response payload: ##### `Boom.preconditionFailed([message], [data])` Returns a 412 Precondition Failed error where: + - `message` - optional message. - `data` - optional additional error data. @@ -404,6 +421,7 @@ Generates the following response payload: ##### `Boom.entityTooLarge([message], [data])` Returns a 413 Request Entity Too Large error where: + - `message` - optional message. - `data` - optional additional error data. @@ -424,6 +442,7 @@ Generates the following response payload: ##### `Boom.uriTooLong([message], [data])` Returns a 414 Request-URI Too Large error where: + - `message` - optional message. - `data` - optional additional error data. @@ -444,6 +463,7 @@ Generates the following response payload: ##### `Boom.unsupportedMediaType([message], [data])` Returns a 415 Unsupported Media Type error where: + - `message` - optional message. - `data` - optional additional error data. @@ -464,6 +484,7 @@ Generates the following response payload: ##### `Boom.rangeNotSatisfiable([message], [data])` Returns a 416 Requested Range Not Satisfiable error where: + - `message` - optional message. - `data` - optional additional error data. @@ -483,6 +504,7 @@ Generates the following response payload: ##### `Boom.expectationFailed([message], [data])` Returns a 417 Expectation Failed error where: + - `message` - optional message. - `data` - optional additional error data. @@ -503,6 +525,7 @@ Generates the following response payload: ##### `Boom.teapot([message], [data])` Returns a 418 I'm a Teapot error where: + - `message` - optional message. - `data` - optional additional error data. @@ -523,6 +546,7 @@ Generates the following response payload: ##### `Boom.badData([message], [data])` Returns a 422 Unprocessable Entity error where: + - `message` - optional message. - `data` - optional additional error data. @@ -543,6 +567,7 @@ Generates the following response payload: ##### `Boom.locked([message], [data])` Returns a 423 Locked error where: + - `message` - optional message. - `data` - optional additional error data. @@ -563,6 +588,7 @@ Generates the following response payload: ##### `Boom.failedDependency([message], [data])` Returns a 424 Failed Dependency error where: + - `message` - optional message. - `data` - optional additional error data. @@ -583,6 +609,7 @@ Generates the following response payload: ##### `Boom.tooEarly([message], [data])` Returns a 425 Too Early error where: + - `message` - optional message. - `data` - optional additional error data. @@ -603,6 +630,7 @@ Generates the following response payload: ##### `Boom.preconditionRequired([message], [data])` Returns a 428 Precondition Required error where: + - `message` - optional message. - `data` - optional additional error data. @@ -623,6 +651,7 @@ Generates the following response payload: ##### `Boom.tooManyRequests([message], [data])` Returns a 429 Too Many Requests error where: + - `message` - optional message. - `data` - optional additional error data. @@ -643,6 +672,7 @@ Generates the following response payload: ##### `Boom.illegal([message], [data])` Returns a 451 Unavailable For Legal Reasons error where: + - `message` - optional message. - `data` - optional additional error data. @@ -664,9 +694,10 @@ Generates the following response payload: All 500 errors hide your message from the end user. -##### `Boom.badImplementation([message], [data])` - (*alias: `internal`*) +##### `Boom.badImplementation([message], [data])` - (_alias: `internal`_) Returns a 500 Internal Server Error error where: + - `message` - optional message. - `data` - optional additional error data. Used as `cause` when when an `Error`. @@ -687,6 +718,7 @@ Generates the following response payload: ##### `Boom.notImplemented([message], [data])` Returns a 501 Not Implemented error where: + - `message` - optional message. - `data` - optional additional error data. Used as `cause` when when an `Error`. @@ -707,6 +739,7 @@ Generates the following response payload: ##### `Boom.badGateway([message], [data])` Returns a 502 Bad Gateway error where: + - `message` - optional message. - `data` - optional additional error data. Used as `cause` when when an `Error`. @@ -727,6 +760,7 @@ Generates the following response payload: ##### `Boom.serverUnavailable([message], [data])` Returns a 503 Service Unavailable error where: + - `message` - optional message. - `data` - optional additional error data. Used as `cause` when when an `Error`. @@ -747,6 +781,7 @@ Generates the following response payload: ##### `Boom.gatewayTimeout([message], [data])` Returns a 504 Gateway Time-out error where: + - `message` - optional message. - `data` - optional additional error data. Used as `cause` when when an `Error`. diff --git a/lib/index.d.ts b/lib/index.d.ts deleted file mode 100755 index 404529c..0000000 --- a/lib/index.d.ts +++ /dev/null @@ -1,559 +0,0 @@ -type NotUnknown = string | number | boolean | bigint | symbol | null | object; - -declare namespace Boom { - type CtorArgs = [message?: string, options?: Options]; - type WithDataArgs = [message: string, options: Options & { data: Data }]; -} - -/** - * An Error object used to return an HTTP response error (4xx, 5xx) - */ -export class Boom extends Error { - - /** - * Creates a new Boom object using the provided message or Error - */ - constructor(...args: Data extends NotUnknown ? Boom.WithDataArgs : Boom.CtorArgs); - - /** - * Underlying cause for the Boom error - */ - cause?: unknown; - - /** - * Custom error data with additional information specific to the error type - */ - data: Data; - - /** - * isBoom - true, indicates this is a Boom object instance. - */ - readonly isBoom: boolean; - - /** - * Convenience boolean indicating status code >= 500 - */ - readonly isServer: boolean; - - /** - * The error message - */ - message: string; - - /** - * The formatted response - */ - output: Output; - - /** - * Specifies if an error object is a valid boom object - * - * @param debug - A boolean that, when true, does not hide the original 500 error message. Defaults to false. - */ - reformat(debug?: boolean): void; -} - -export interface BaseOptions { - /** - * The HTTP status code - * - * @default 500 - */ - readonly statusCode?: number; - - /** - * Additional error information - */ - readonly data?: Data; -} - - -export interface Options extends BaseOptions { - /** - * An object containing any HTTP headers where each key is a header name and value is the header content - */ - readonly headers?: { readonly [header: string]: string | readonly string[] | number } | undefined; - - /** - * Constructor reference used to crop the exception call stack output - */ - readonly ctor?: Function; - - /** - * An underlying cause for the Boom error - */ - readonly cause?: Error | unknown | undefined; -} - - -export interface BoomifyOptions extends BaseOptions { - /** - * Error message string - * - * @default none - */ - readonly message?: string; - - /** - * If false, the err provided is a Boom object, and a statusCode or message are provided, the values are ignored - * - * @default true - */ - readonly override?: boolean; -} - - -export interface Payload { - /** - * The HTTP status code derived from error.output.statusCode - */ - readonly statusCode: number; - - /** - * The HTTP status message derived from statusCode - */ - readonly error: string; - - /** - * The error message derived from error.message - */ - readonly message: string; -} - - -export interface Output { - /** - * The HTTP status code - */ - statusCode: number; - - /** - * An object containing any HTTP headers where each key is a header name and value is the header content - */ - headers: { [header: string]: string | string[] | number | undefined }; - - /** - * The formatted object used as the response payload (stringified) - */ - payload: Payload & { [key: string]: unknown }; -} - - -/** -* Specifies if an object is a valid boom object -* -* @param obj - The object to assess -* @param statusCode - Optional status code -* -* @returns Returns a boolean stating if the error object is a valid boom object and it has the provided statusCode (if present) -*/ -export function isBoom(obj: unknown, statusCode?: number): obj is Boom; - - -/** -* Applies options to an existing boom object, or creates a new boom object with the error as `cause` -* -* @param err - The target object -* @param options - Options object -* -* @returns A boom object -*/ -export function boomify & Pick, 'data'>, Terr extends Omit = Boom, Data = unknown>(err: Terr, options: BoomifyOptions & { data: Tres extends Boom ? Data : Data }): Tres; -export function boomify, Terr = any, Data = unknown>(err: Terr, options: BoomifyOptions & { data: Tres extends Boom ? Data : Data }): Tres; -export function boomify = Tres>(err: Terr, options?: BoomifyOptions): Tres; -export function boomify, Terr = any>(err: Terr, options?: BoomifyOptions): Tres; - -// 4xx Errors - -/** -* Returns a 400 Bad Request error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 400 bad request error -*/ -export function badRequest(message?: string, data?: Data): Boom; - - -/** -* Returns a 401 Unauthorized error -* -* @param message - Optional message -* -* @returns A 401 Unauthorized error -*/ -export function unauthorized(message?: string): Boom; - - -/** -* Returns a 401 Unauthorized error -* -* @param message - Optional message -* @param scheme - the authentication scheme name -* @param attributes - an object of values used to construct the 'WWW-Authenticate' header -* -* @returns A 401 Unauthorized error -*/ -export function unauthorized(message: '' | null | undefined, scheme: string, attributes?: string | unauthorized.Attributes): Boom & unauthorized.MissingAuth; -export function unauthorized(message: string, scheme: string, attributes?: string | unauthorized.Attributes): Boom; - - -export namespace unauthorized { - - interface Attributes { - [index: string]: number | string | null | undefined; - } - - interface MissingAuth { - - /** - * Indicate whether the 401 unauthorized error is due to missing credentials (vs. invalid) - */ - isMissing: true; - } -} - - -/** -* Returns a 401 Unauthorized error -* -* @param message - Optional message -* @param wwwAuthenticate - array of string values used to construct the wwwAuthenticate header -* -* @returns A 401 Unauthorized error -*/ -export function unauthorized(message: string | null | undefined, wwwAuthenticate: readonly string[]): Boom; - - -/** -* Returns a 402 Payment Required error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 402 Payment Required error -*/ -export function paymentRequired(message?: string, data?: Data): Boom; - - -/** -* Returns a 403 Forbidden error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 403 Forbidden error -*/ -export function forbidden(message?: string, data?: Data): Boom; - - -/** -* Returns a 404 Not Found error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 404 Not Found error -*/ -export function notFound(message?: string, data?: Data): Boom; - - -/** -* Returns a 405 Method Not Allowed error -* -* @param message - Optional message -* @param data - Optional additional error data -* @param allow - Optional string or array of strings which is used to set the 'Allow' header -* -* @returns A 405 Method Not Allowed error -*/ -export function methodNotAllowed(message?: string, data?: Data, allow?: string | readonly string[]): Boom; - - -/** -* Returns a 406 Not Acceptable error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 406 Not Acceptable error -*/ -export function notAcceptable(message?: string, data?: Data): Boom; - - -/** -* Returns a 407 Proxy Authentication error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 407 Proxy Authentication error -*/ -export function proxyAuthRequired(message?: string, data?: Data): Boom; - - -/** -* Returns a 408 Request Time-out error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 408 Request Time-out error -*/ -export function clientTimeout(message?: string, data?: Data): Boom; - - -/** -* Returns a 409 Conflict error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 409 Conflict error -*/ -export function conflict(message?: string, data?: Data): Boom; - - -/** -* Returns a 410 Gone error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 410 gone error -*/ -export function resourceGone(message?: string, data?: Data): Boom; - - -/** -* Returns a 411 Length Required error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 411 Length Required error -*/ -export function lengthRequired(message?: string, data?: Data): Boom; - - -/** -* Returns a 412 Precondition Failed error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 412 Precondition Failed error -*/ -export function preconditionFailed(message?: string, data?: Data): Boom; - - -/** -* Returns a 413 Request Entity Too Large error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 413 Request Entity Too Large error -*/ -export function entityTooLarge(message?: string, data?: Data): Boom; - - -/** -* Returns a 414 Request-URI Too Large error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 414 Request-URI Too Large error -*/ -export function uriTooLong(message?: string, data?: Data): Boom; - - -/** -* Returns a 415 Unsupported Media Type error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 415 Unsupported Media Type error -*/ -export function unsupportedMediaType(message?: string, data?: Data): Boom; - - -/** -* Returns a 416 Request Range Not Satisfiable error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 416 Request Range Not Satisfiable error -*/ -export function rangeNotSatisfiable(message?: string, data?: Data): Boom; - - -/** -* Returns a 417 Expectation Failed error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 417 Expectation Failed error -*/ -export function expectationFailed(message?: string, data?: Data): Boom; - - -/** -* Returns a 418 I'm a Teapot error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 418 I'm a Teapot error -*/ -export function teapot(message?: string, data?: Data): Boom; - - -/** -* Returns a 422 Unprocessable Entity error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 422 Unprocessable Entity error -*/ -export function badData(message?: string, data?: Data): Boom; - - -/** -* Returns a 423 Locked error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 423 Locked error -*/ -export function locked(message?: string, data?: Data): Boom; - - -/** -* Returns a 424 Failed Dependency error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 424 Failed Dependency error -*/ -export function failedDependency(message?: string, data?: Data): Boom; - -/** -* Returns a 425 Too Early error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 425 Too Early error -*/ -export function tooEarly(message?: string, data?: Data): Boom; - - -/** -* Returns a 428 Precondition Required error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 428 Precondition Required error -*/ -export function preconditionRequired(message?: string, data?: Data): Boom; - - -/** -* Returns a 429 Too Many Requests error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 429 Too Many Requests error -*/ -export function tooManyRequests(message?: string, data?: Data): Boom; - - -/** -* Returns a 451 Unavailable For Legal Reasons error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 451 Unavailable for Legal Reasons error -*/ -export function illegal(message?: string, data?: Data): Boom; - - -// 5xx Errors - -/** -* Returns a internal error (defaults to 500) -* -* @param message - Optional message -* @param data - Optional additional error data -* @param statusCode - Optional status code override. Defaults to 500. -* -* @returns A 500 Internal Server error -*/ -export function internal(message?: string, data?: Data | Error, statusCode?: number): Boom; - - -/** -* Returns a 500 Internal Server Error error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 500 Internal Server error -*/ -export function badImplementation(message?: string, data?: Data | Error): Boom; - - -/** -* Returns a 501 Not Implemented error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 501 Not Implemented error -*/ -export function notImplemented(message?: string, data?: Data | Error): Boom; - - -/** -* Returns a 502 Bad Gateway error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 502 Bad Gateway error -*/ -export function badGateway(message?: string, data?: Data | Error): Boom; - - -/** -* Returns a 503 Service Unavailable error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 503 Service Unavailable error -*/ -export function serverUnavailable(message?: string, data?: Data | Error): Boom; - - -/** -* Returns a 504 Gateway Time-out error -* -* @param message - Optional message -* @param data - Optional additional error data -* -* @returns A 504 Gateway Time-out error -*/ -export function gatewayTimeout(message?: string, data?: Data | Error): Boom; diff --git a/lib/index.js b/lib/index.js deleted file mode 100755 index 39627c1..0000000 --- a/lib/index.js +++ /dev/null @@ -1,427 +0,0 @@ -'use strict'; - -const EscapeHeaderAttribute = require('@hapi/hoek/escapeHeaderAttribute'); - - -const internals = { - codes: new Map([ - [100, 'Continue'], - [101, 'Switching Protocols'], - [102, 'Processing'], - [200, 'OK'], - [201, 'Created'], - [202, 'Accepted'], - [203, 'Non-Authoritative Information'], - [204, 'No Content'], - [205, 'Reset Content'], - [206, 'Partial Content'], - [207, 'Multi-Status'], - [300, 'Multiple Choices'], - [301, 'Moved Permanently'], - [302, 'Moved Temporarily'], - [303, 'See Other'], - [304, 'Not Modified'], - [305, 'Use Proxy'], - [307, 'Temporary Redirect'], - [400, 'Bad Request'], - [401, 'Unauthorized'], - [402, 'Payment Required'], - [403, 'Forbidden'], - [404, 'Not Found'], - [405, 'Method Not Allowed'], - [406, 'Not Acceptable'], - [407, 'Proxy Authentication Required'], - [408, 'Request Time-out'], - [409, 'Conflict'], - [410, 'Gone'], - [411, 'Length Required'], - [412, 'Precondition Failed'], - [413, 'Request Entity Too Large'], - [414, 'Request-URI Too Large'], - [415, 'Unsupported Media Type'], - [416, 'Requested Range Not Satisfiable'], - [417, 'Expectation Failed'], - [418, 'I\'m a teapot'], - [422, 'Unprocessable Entity'], - [423, 'Locked'], - [424, 'Failed Dependency'], - [425, 'Too Early'], - [426, 'Upgrade Required'], - [428, 'Precondition Required'], - [429, 'Too Many Requests'], - [431, 'Request Header Fields Too Large'], - [451, 'Unavailable For Legal Reasons'], - [500, 'Internal Server Error'], - [501, 'Not Implemented'], - [502, 'Bad Gateway'], - [503, 'Service Unavailable'], - [504, 'Gateway Time-out'], - [505, 'HTTP Version Not Supported'], - [506, 'Variant Also Negotiates'], - [507, 'Insufficient Storage'], - [509, 'Bandwidth Limit Exceeded'], - [510, 'Not Extended'], - [511, 'Network Authentication Required'] - ]) -}; - - -exports.Boom = class Boom extends Error { - - data = null; - output; - - get isServer() { - - return this.output.statusCode >= 500; - } - - set isServer(value) { // Allow for compatiblity with legacy boom - - Object.defineProperty(this, 'isServer', { - value, - writable: true, - configurable: true - }); - } - - constructor(message, options = {}) { - - const { statusCode = 500, data, headers, ctor = exports.Boom } = options; - const causeOption = 'cause' in options ? { cause: options.cause } : undefined; - - super(message ?? internals.codes.get(statusCode) ?? 'Unknown', causeOption); - if (typeof Error.captureStackTrace === 'function') { // Only use when available - Error.captureStackTrace(this, ctor); // Filter the stack to our external API - } - - if (causeOption) { - this.cause ??= causeOption.cause; // Explicitly assign cause to work with old runtimes - } - - internals.apply(this, data, statusCode, headers); - } - - static [Symbol.hasInstance](instance) { - - if (this === exports.Boom) { - return exports.isBoom(instance); - } - - // Cannot use 'instanceof' as it creates infinite recursion - - return this.prototype.isPrototypeOf(instance); - } - - reformat(debug = false) { - - this.output.payload = new internals.PayloadObject(this, this.output.statusCode, debug); - } - - static { - Object.defineProperties(this.prototype, { - name: { value: 'Boom', writable: true, configurable: true }, - isBoom: { value: true, writable: true, configurable: true } - }); - } -}; - - -exports.isBoom = function (err, statusCode) { - - return err instanceof Error && !!err.isBoom && (!statusCode || err.output.statusCode === statusCode); -}; - - -exports.boomify = function (err, options = {}) { - - const { override, data, statusCode, message } = options; - - if (!err?.isBoom === true) { - return new exports.Boom(message, { statusCode, cause: err, data }); - } - - if (override === false) { // Defaults to true - internals.apply(err, data); - } - else { - internals.apply(err, data, statusCode ?? err.output.statusCode, {}, message); - } - - if (err.hasOwnProperty('isServer')) { - err.isServer = err.output.statusCode >= 500; // Assign, in case it is a legacy boom object - } - - return err; -}; - - -internals.apply = function (boom, data, statusCode, headers, message) { - - if (data !== undefined) { - boom.data = data; - } - - if (statusCode) { - const numberCode = parseInt(statusCode, 10); - if (isNaN(numberCode) || numberCode < 400) { - throw new TypeError(`statusCode must be a number (400+): ${statusCode}`); - } - - if (headers !== undefined && - (headers === null || typeof headers !== 'object')) { - - throw new TypeError('headers must be an object'); - } - - if (message) { - boom.message = `${message}: ${boom.message}`; - } - - boom.output = new internals.BoomOutput(numberCode, headers); - boom.reformat(); - } -}; - - -internals.PayloadObject = class { - - statusCode; - error; - message; - - constructor(error, statusCode, debug) { - - this.statusCode = statusCode; - this.error = internals.codes.get(statusCode) ?? 'Unknown'; - - if (statusCode === 500 && debug !== true) { - this.message = 'An internal server error occurred'; // Hide actual error from user - } - else { - this.message = error.message; - if (error.cause) { - const message = error.cause.message ?? error.cause; - this.message = (error.message === this.error) ? message : error.message + ': ' + message; - } - } - } -}; - - -internals.BoomOutput = class { - - statusCode; - payload = {}; - headers; - - constructor(statusCode, headers) { - - this.statusCode = statusCode; - - const copy = Object.assign(Object.create(null), headers); - for (const [key, value] of Object.entries(copy)) { - if (Array.isArray(value)) { - copy[key] = value.slice(); - } - } - - this.headers = copy; - } -}; - - -internals.statusError = function (statusCode, massage) { - - const method = massage ? - function (...args) { - - const [message, options, decorate] = massage(...args); - return Object.assign(new exports.Boom(message, { statusCode, ctor: method, ...options }), decorate); - } : - function (message, data) { - - return new exports.Boom(message, { statusCode, data, ctor: method }); - }; - - return method; -}; - - -// 4xx Client Errors - -exports.badRequest = internals.statusError(400); - - -exports.unauthorized = internals.statusError(401, (message, scheme, attributes) => { // Or (message, wwwAuthenticate[]) - - // function (message) - - if (!scheme) { - return [message]; - } - - // function (message, wwwAuthenticate[]) - - if (typeof scheme !== 'string') { - const headers = { 'WWW-Authenticate': scheme.join(', ') }; - return [message, { headers }]; - } - - // function (message, scheme, attributes) - - const decorate = {}; - let stringified = ''; - - if (attributes) { - if (typeof attributes === 'string') { - stringified += EscapeHeaderAttribute(attributes); - } - else { - stringified += Object.keys(attributes).map((name) => { - - const value = attributes[name] ?? ''; - - return `${name}="${EscapeHeaderAttribute(value.toString())}"`; - }) - .join(', '); - } - } - - if (message) { - if (stringified) { - stringified += ', '; - } - - stringified += `error="${EscapeHeaderAttribute(message)}"`; - } - else { - decorate.isMissing = true; - } - - const headers = { 'WWW-Authenticate': stringified ? `${scheme} ${stringified}` : `${scheme}` }; - return [message, { headers }, decorate]; -}); - - -exports.paymentRequired = internals.statusError(402); - - -exports.forbidden = internals.statusError(403); - - -exports.notFound = internals.statusError(404); - - -exports.methodNotAllowed = internals.statusError(405, (message, data, allow) => { - - if (typeof allow === 'string') { - allow = [allow]; - } - - const headers = Array.isArray(allow) ? { - Allow: allow.join(', ') - } : undefined; - - return [message, { data, headers }]; -}); - - -exports.notAcceptable = internals.statusError(406); - - -exports.proxyAuthRequired = internals.statusError(407); - - -exports.clientTimeout = internals.statusError(408); - - -exports.conflict = internals.statusError(409); - - -exports.resourceGone = internals.statusError(410); - - -exports.lengthRequired = internals.statusError(411); - - -exports.preconditionFailed = internals.statusError(412); - - -exports.entityTooLarge = internals.statusError(413); - - -exports.uriTooLong = internals.statusError(414); - - -exports.unsupportedMediaType = internals.statusError(415); - - -exports.rangeNotSatisfiable = internals.statusError(416); - - -exports.expectationFailed = internals.statusError(417); - - -exports.teapot = internals.statusError(418); - - -exports.badData = internals.statusError(422); - - -exports.locked = internals.statusError(423); - - -exports.failedDependency = internals.statusError(424); - - -exports.tooEarly = internals.statusError(425); - - -exports.preconditionRequired = internals.statusError(428); - - -exports.tooManyRequests = internals.statusError(429); - - -exports.illegal = internals.statusError(451); - - -// 5xx Server Errors - -internals.serverError = function (message, data) { - - const isDataNonBoomError = data instanceof Error && !exports.isBoom(data); - - return [message, isDataNonBoomError ? { cause: data } : { data }]; -}; - - -exports.internal = internals.statusError(500, (message, data, statusCode = 500) => { - - const res = internals.serverError(message, data); - if (statusCode !== 500) { - const [, options] = res; - options.statusCode = statusCode; - } - - return res; -}); - - -exports.notImplemented = internals.statusError(501, internals.serverError); - - -exports.badGateway = internals.statusError(502, internals.serverError); - - -exports.serverUnavailable = internals.statusError(503, internals.serverError); - - -exports.gatewayTimeout = internals.statusError(504, internals.serverError); - - -exports.badImplementation = internals.statusError(500, (message, data) => { - - return [...internals.serverError(message, data), { isDeveloperError: true }]; -}); diff --git a/oxfmt.config.ts b/oxfmt.config.ts new file mode 100644 index 0000000..12e357b --- /dev/null +++ b/oxfmt.config.ts @@ -0,0 +1,8 @@ +import DefaultOxfmtConfig from '@hapi/oxc-plugin/oxfmt'; +import { defineConfig } from 'oxfmt'; + +import type { OxfmtConfig } from 'oxfmt'; + +export default defineConfig({ + ...DefaultOxfmtConfig, +}) as OxfmtConfig; diff --git a/oxlint.config.ts b/oxlint.config.ts new file mode 100644 index 0000000..fd4548e --- /dev/null +++ b/oxlint.config.ts @@ -0,0 +1,11 @@ +import HapiRecommended from '@hapi/oxc-plugin/oxlint'; +import { defineConfig } from 'oxlint'; + +import type { OxlintConfig } from 'oxlint'; + +export default defineConfig({ + extends: [HapiRecommended], + env: { + ...HapiRecommended.env, + }, +}) as OxlintConfig; diff --git a/package.json b/package.json index 099707c..48616ef 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,50 @@ { - "name": "@hapi/boom", - "description": "HTTP-friendly error objects", - "version": "10.0.1", - "repository": "git://github.com/hapijs/boom", - "main": "lib/index.js", - "types": "lib/index.d.ts", - "keywords": [ - "error", - "http" - ], - "files": [ - "lib" - ], - "eslintConfig": { - "extends": [ - "plugin:@hapi/module" - ] - }, - "dependencies": { - "@hapi/hoek": "^11.0.2" - }, - "devDependencies": { - "@hapi/boom10": "npm:@hapi/boom@^10.0.1", - "@hapi/code": "9.x.x", - "@hapi/eslint-plugin": "^7.0.0", - "@hapi/lab": "^26.0.0", - "@types/node": "^18.19.3", - "typescript": "~5.6.3" - }, - "scripts": { - "test": "lab -a @hapi/code -t 100 -L -Y", - "test-cov-html": "lab -a @hapi/code -t 100 -L -r html -o coverage.html" - }, - "license": "BSD-3-Clause" + "name": "@hapi/boom", + "version": "10.0.1", + "description": "HTTP-friendly error objects", + "keywords": [ + "error", + "http" + ], + "license": "BSD-3-Clause", + "repository": { + "type": "git", + "url": "git://github.com/hapijs/boom.git" + }, + "files": [ + "src", + "API.md" + ], + "type": "module", + "types": "src/index.d.ts", + "exports": { + ".": { + "types": "./src/index.d.ts", + "default": "./src/index.js" + } + }, + "scripts": { + "test": "vitest run --coverage", + "typecheck": "tsc --noEmit", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "fmt": "oxfmt --check", + "fmt:fix": "oxfmt", + "check": "npm run lint && npm run fmt && npm run typecheck && npm test" + }, + "dependencies": { + "@hapi/hoek": "^12.0.0-rc.0" + }, + "devDependencies": { + "@hapi/boom10": "npm:@hapi/boom@^10.0.1", + "@hapi/oxc-plugin": "^1.0.4", + "@vitest/coverage-v8": "^4.1.10", + "oxfmt": "^0.61.0", + "oxlint": "^1.76.0", + "typescript": "^6.0.3", + "vitest": "^4.1.10" + }, + "engines": { + "node": ">=22" + } } diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100755 index 0000000..8f3c752 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,459 @@ +type NotUnknown = string | number | boolean | bigint | symbol | null | object; + +declare namespace Boom { + type CtorArgs = [message?: string, options?: Options]; + type WithDataArgs = [message: string, options: Options & { data: Data }]; +} + +/** An Error object used to return an HTTP response error (4xx, 5xx) */ +export class Boom extends Error { + /** Creates a new Boom object using the provided message or Error */ + constructor(...args: Data extends NotUnknown ? Boom.WithDataArgs : Boom.CtorArgs); + + /** Underlying cause for the Boom error */ + cause?: unknown; + + /** Custom error data with additional information specific to the error type */ + data: Data; + + /** IsBoom - true, indicates this is a Boom object instance. */ + readonly isBoom: boolean; + + /** Convenience boolean indicating status code >= 500 */ + readonly isServer: boolean; + + /** The error message */ + message: string; + + /** The formatted response */ + output: Output; + + /** + * Specifies if an error object is a valid boom object + * + * @param debug - A boolean that, when true, does not hide the original 500 error message. Defaults to false. + */ + reformat(debug?: boolean): void; +} + +export interface BaseOptions { + /** + * The HTTP status code + * + * @default 500 + */ + readonly statusCode?: number; + + /** Additional error information */ + readonly data?: Data; +} + +export interface Options extends BaseOptions { + /** An object containing any HTTP headers where each key is a header name and value is the header content */ + readonly headers?: { readonly [header: string]: string | readonly string[] | number } | undefined; + + /** Constructor reference used to crop the exception call stack output */ + readonly ctor?: Function; + + /** An underlying cause for the Boom error */ + readonly cause?: Error | unknown | undefined; +} + +export interface BoomifyOptions extends BaseOptions { + /** + * Error message string + * + * @default none + */ + readonly message?: string; + + /** + * If false, the err provided is a Boom object, and a statusCode or message are provided, the values are ignored + * + * @default true + */ + readonly override?: boolean; +} + +export interface Payload { + /** The HTTP status code derived from error.output.statusCode */ + readonly statusCode: number; + + /** The HTTP status message derived from statusCode */ + readonly error: string; + + /** The error message derived from error.message */ + readonly message: string; +} + +export interface Output { + /** The HTTP status code */ + statusCode: number; + + /** An object containing any HTTP headers where each key is a header name and value is the header content */ + headers: { [header: string]: string | string[] | number | undefined }; + + /** The formatted object used as the response payload (stringified) */ + payload: Payload & { [key: string]: unknown }; +} + +/** + * Specifies if an object is a valid boom object + * + * @param obj - The object to assess + * @param statusCode - Optional status code + * @returns Returns a boolean stating if the error object is a valid boom object and it has the provided statusCode (if + * present) + */ +export function isBoom(obj: unknown, statusCode?: number): obj is Boom; + +/** + * Applies options to an existing boom object, or creates a new boom object with the error as `cause` + * + * @param err - The target object + * @param options - Options object + * @returns A boom object + */ +export function boomify< + Tres extends Omit & Pick, 'data'>, + Terr extends Omit = Boom, + Data = unknown, +>(err: Terr, options: BoomifyOptions & { data: Tres extends Boom ? Data : Data }): Tres; +export function boomify, Terr = any, Data = unknown>( + err: Terr, + options: BoomifyOptions & { data: Tres extends Boom ? Data : Data }, +): Tres; +export function boomify = Tres>(err: Terr, options?: BoomifyOptions): Tres; +export function boomify, Terr = any>(err: Terr, options?: BoomifyOptions): Tres; + +// 4xx Errors + +/** + * Returns a 400 Bad Request error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 400 bad request error + */ +export function badRequest(message?: string, data?: Data): Boom; + +/** + * Returns a 401 Unauthorized error + * + * @param message - Optional message + * @returns A 401 Unauthorized error + */ +export function unauthorized(message?: string): Boom; + +/** + * Returns a 401 Unauthorized error + * + * @param message - Optional message + * @param scheme - The authentication scheme name + * @param attributes - An object of values used to construct the 'WWW-Authenticate' header + * @returns A 401 Unauthorized error + */ +export function unauthorized( + message: '' | null | undefined, + scheme: string, + attributes?: string | unauthorized.Attributes, +): Boom & unauthorized.MissingAuth; +export function unauthorized( + message: string, + scheme: string, + attributes?: string | unauthorized.Attributes, +): Boom; + +export namespace unauthorized { + interface Attributes { + [index: string]: number | string | null | undefined; + } + + interface MissingAuth { + /** Indicate whether the 401 unauthorized error is due to missing credentials (vs. invalid) */ + isMissing: true; + } +} + +/** + * Returns a 401 Unauthorized error + * + * @param message - Optional message + * @param wwwAuthenticate - Array of string values used to construct the wwwAuthenticate header + * @returns A 401 Unauthorized error + */ +export function unauthorized(message: string | null | undefined, wwwAuthenticate: readonly string[]): Boom; + +/** + * Returns a 402 Payment Required error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 402 Payment Required error + */ +export function paymentRequired(message?: string, data?: Data): Boom; + +/** + * Returns a 403 Forbidden error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 403 Forbidden error + */ +export function forbidden(message?: string, data?: Data): Boom; + +/** + * Returns a 404 Not Found error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 404 Not Found error + */ +export function notFound(message?: string, data?: Data): Boom; + +/** + * Returns a 405 Method Not Allowed error + * + * @param message - Optional message + * @param data - Optional additional error data + * @param allow - Optional string or array of strings which is used to set the 'Allow' header + * @returns A 405 Method Not Allowed error + */ +export function methodNotAllowed(message?: string, data?: Data, allow?: string | readonly string[]): Boom; + +/** + * Returns a 406 Not Acceptable error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 406 Not Acceptable error + */ +export function notAcceptable(message?: string, data?: Data): Boom; + +/** + * Returns a 407 Proxy Authentication error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 407 Proxy Authentication error + */ +export function proxyAuthRequired(message?: string, data?: Data): Boom; + +/** + * Returns a 408 Request Time-out error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 408 Request Time-out error + */ +export function clientTimeout(message?: string, data?: Data): Boom; + +/** + * Returns a 409 Conflict error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 409 Conflict error + */ +export function conflict(message?: string, data?: Data): Boom; + +/** + * Returns a 410 Gone error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 410 gone error + */ +export function resourceGone(message?: string, data?: Data): Boom; + +/** + * Returns a 411 Length Required error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 411 Length Required error + */ +export function lengthRequired(message?: string, data?: Data): Boom; + +/** + * Returns a 412 Precondition Failed error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 412 Precondition Failed error + */ +export function preconditionFailed(message?: string, data?: Data): Boom; + +/** + * Returns a 413 Request Entity Too Large error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 413 Request Entity Too Large error + */ +export function entityTooLarge(message?: string, data?: Data): Boom; + +/** + * Returns a 414 Request-URI Too Large error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 414 Request-URI Too Large error + */ +export function uriTooLong(message?: string, data?: Data): Boom; + +/** + * Returns a 415 Unsupported Media Type error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 415 Unsupported Media Type error + */ +export function unsupportedMediaType(message?: string, data?: Data): Boom; + +/** + * Returns a 416 Request Range Not Satisfiable error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 416 Request Range Not Satisfiable error + */ +export function rangeNotSatisfiable(message?: string, data?: Data): Boom; + +/** + * Returns a 417 Expectation Failed error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 417 Expectation Failed error + */ +export function expectationFailed(message?: string, data?: Data): Boom; + +/** + * Returns a 418 I'm a Teapot error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 418 I'm a Teapot error + */ +export function teapot(message?: string, data?: Data): Boom; + +/** + * Returns a 422 Unprocessable Entity error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 422 Unprocessable Entity error + */ +export function badData(message?: string, data?: Data): Boom; + +/** + * Returns a 423 Locked error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 423 Locked error + */ +export function locked(message?: string, data?: Data): Boom; + +/** + * Returns a 424 Failed Dependency error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 424 Failed Dependency error + */ +export function failedDependency(message?: string, data?: Data): Boom; + +/** + * Returns a 425 Too Early error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 425 Too Early error + */ +export function tooEarly(message?: string, data?: Data): Boom; + +/** + * Returns a 428 Precondition Required error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 428 Precondition Required error + */ +export function preconditionRequired(message?: string, data?: Data): Boom; + +/** + * Returns a 429 Too Many Requests error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 429 Too Many Requests error + */ +export function tooManyRequests(message?: string, data?: Data): Boom; + +/** + * Returns a 451 Unavailable For Legal Reasons error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 451 Unavailable for Legal Reasons error + */ +export function illegal(message?: string, data?: Data): Boom; + +// 5xx Errors + +/** + * Returns a internal error (defaults to 500) + * + * @param message - Optional message + * @param data - Optional additional error data + * @param statusCode - Optional status code override. Defaults to 500. + * @returns A 500 Internal Server error + */ +export function internal(message?: string, data?: Data | Error, statusCode?: number): Boom; + +/** + * Returns a 500 Internal Server Error error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 500 Internal Server error + */ +export function badImplementation(message?: string, data?: Data | Error): Boom; + +/** + * Returns a 501 Not Implemented error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 501 Not Implemented error + */ +export function notImplemented(message?: string, data?: Data | Error): Boom; + +/** + * Returns a 502 Bad Gateway error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 502 Bad Gateway error + */ +export function badGateway(message?: string, data?: Data | Error): Boom; + +/** + * Returns a 503 Service Unavailable error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 503 Service Unavailable error + */ +export function serverUnavailable(message?: string, data?: Data | Error): Boom; + +/** + * Returns a 504 Gateway Time-out error + * + * @param message - Optional message + * @param data - Optional additional error data + * @returns A 504 Gateway Time-out error + */ +export function gatewayTimeout(message?: string, data?: Data | Error): Boom; diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..7b7934b --- /dev/null +++ b/src/index.js @@ -0,0 +1,365 @@ +import { escapeHeaderAttribute } from '@hapi/hoek'; + +const codes = new Map([ + [100, 'Continue'], + [101, 'Switching Protocols'], + [102, 'Processing'], + [200, 'OK'], + [201, 'Created'], + [202, 'Accepted'], + [203, 'Non-Authoritative Information'], + [204, 'No Content'], + [205, 'Reset Content'], + [206, 'Partial Content'], + [207, 'Multi-Status'], + [300, 'Multiple Choices'], + [301, 'Moved Permanently'], + [302, 'Moved Temporarily'], + [303, 'See Other'], + [304, 'Not Modified'], + [305, 'Use Proxy'], + [307, 'Temporary Redirect'], + [400, 'Bad Request'], + [401, 'Unauthorized'], + [402, 'Payment Required'], + [403, 'Forbidden'], + [404, 'Not Found'], + [405, 'Method Not Allowed'], + [406, 'Not Acceptable'], + [407, 'Proxy Authentication Required'], + [408, 'Request Time-out'], + [409, 'Conflict'], + [410, 'Gone'], + [411, 'Length Required'], + [412, 'Precondition Failed'], + [413, 'Request Entity Too Large'], + [414, 'Request-URI Too Large'], + [415, 'Unsupported Media Type'], + [416, 'Requested Range Not Satisfiable'], + [417, 'Expectation Failed'], + [418, "I'm a teapot"], + [422, 'Unprocessable Entity'], + [423, 'Locked'], + [424, 'Failed Dependency'], + [425, 'Too Early'], + [426, 'Upgrade Required'], + [428, 'Precondition Required'], + [429, 'Too Many Requests'], + [431, 'Request Header Fields Too Large'], + [451, 'Unavailable For Legal Reasons'], + [500, 'Internal Server Error'], + [501, 'Not Implemented'], + [502, 'Bad Gateway'], + [503, 'Service Unavailable'], + [504, 'Gateway Time-out'], + [505, 'HTTP Version Not Supported'], + [506, 'Variant Also Negotiates'], + [507, 'Insufficient Storage'], + [509, 'Bandwidth Limit Exceeded'], + [510, 'Not Extended'], + [511, 'Network Authentication Required'], +]); + +export class Boom extends Error { + data = null; + output; + + get isServer() { + return this.output.statusCode >= 500; + } + + // Allow for compatiblity with legacy boom + + set isServer(value) { + Object.defineProperty(this, 'isServer', { + value, + writable: true, + configurable: true, + }); + } + + constructor(message, options = {}) { + const { statusCode = 500, data, headers, ctor = Boom } = options; + const causeOption = 'cause' in options ? { cause: options.cause } : undefined; + + super(message ?? codes.get(statusCode) ?? 'Unknown', causeOption); + // Only use Error.captureStackTrace when available + + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, ctor); // Filter the stack to our external API + } + + if (causeOption) { + this.cause ??= causeOption.cause; // Explicitly assign cause to work with old runtimes + } + + apply(this, data, statusCode, headers); + } + + static [Symbol.hasInstance](instance) { + if (this === Boom) { + return isBoom(instance); + } + + // Cannot use 'instanceof' as it creates infinite recursion + + return this.prototype.isPrototypeOf(instance); + } + + reformat(debug = false) { + this.output.payload = new PayloadObject(this, this.output.statusCode, debug); + } + + static { + Object.defineProperties(this.prototype, { + name: { value: 'Boom', writable: true, configurable: true }, + isBoom: { value: true, writable: true, configurable: true }, + }); + } +} + +export function isBoom(err, statusCode) { + return err instanceof Error && !!err.isBoom && (!statusCode || err.output.statusCode === statusCode); +} + +export function boomify(err, options = {}) { + const { override, data, statusCode, message } = options; + + if (!err?.isBoom === true) { + return new Boom(message, { statusCode, cause: err, data }); + } + + // override defaults to true + + if (override === false) { + apply(err, data); + } else { + apply(err, data, statusCode ?? err.output.statusCode, {}, message); + } + + if (Object.hasOwn(err, 'isServer')) { + err.isServer = err.output.statusCode >= 500; // Assign, in case it is a legacy boom object + } + + return err; +} + +function apply(boom, data, statusCode, headers, message) { + if (data !== undefined) { + boom.data = data; + } + + if (statusCode) { + const numberCode = parseInt(statusCode, 10); + if (isNaN(numberCode) || numberCode < 400) { + throw new TypeError(`statusCode must be a number (400+): ${statusCode}`); + } + + if (headers !== undefined && (headers === null || typeof headers !== 'object')) { + throw new TypeError('headers must be an object'); + } + + if (message) { + boom.message = `${message}: ${boom.message}`; + } + + boom.output = new BoomOutput(numberCode, headers); + boom.reformat(); + } +} + +class PayloadObject { + statusCode; + error; + message; + + constructor(error, statusCode, debug) { + this.statusCode = statusCode; + this.error = codes.get(statusCode) ?? 'Unknown'; + + if (statusCode === 500 && debug !== true) { + this.message = 'An internal server error occurred'; // Hide actual error from user + } else { + this.message = error.message; + if (error.cause) { + const message = error.cause.message ?? error.cause; + this.message = error.message === this.error ? message : error.message + ': ' + message; + } + } + } +} + +class BoomOutput { + statusCode; + payload = {}; + headers; + + constructor(statusCode, headers) { + this.statusCode = statusCode; + + const copy = Object.assign(Object.create(null), headers); + for (const [key, value] of Object.entries(copy)) { + if (Array.isArray(value)) { + copy[key] = value.slice(); + } + } + + this.headers = copy; + } +} + +function statusError(statusCode, massage) { + const method = massage + ? function (...args) { + const [message, options, decorate] = massage(...args); + return Object.assign(new Boom(message, { statusCode, ctor: method, ...options }), decorate); + } + : function (message, data) { + return new Boom(message, { statusCode, data, ctor: method }); + }; + + return method; +} + +// 4xx Client Errors + +export const badRequest = statusError(400); + +// Or (message, wwwAuthenticate[]) + +export const unauthorized = statusError(401, (message, scheme, attributes) => { + // function (message) + + if (!scheme) { + return [message]; + } + + // function (message, wwwAuthenticate[]) + + if (typeof scheme !== 'string') { + const headers = { 'WWW-Authenticate': scheme.join(', ') }; + return [message, { headers }]; + } + + // function (message, scheme, attributes) + + const decorate = {}; + let stringified = ''; + + if (attributes) { + if (typeof attributes === 'string') { + stringified += escapeHeaderAttribute(attributes); + } else { + stringified += Object.keys(attributes) + .map((name) => { + const value = attributes[name] ?? ''; + + return `${name}="${escapeHeaderAttribute(value.toString())}"`; + }) + .join(', '); + } + } + + if (message) { + if (stringified) { + stringified += ', '; + } + + stringified += `error="${escapeHeaderAttribute(message)}"`; + } else { + decorate.isMissing = true; + } + + const headers = { 'WWW-Authenticate': stringified ? `${scheme} ${stringified}` : `${scheme}` }; + return [message, { headers }, decorate]; +}); + +export const paymentRequired = statusError(402); + +export const forbidden = statusError(403); + +export const notFound = statusError(404); + +export const methodNotAllowed = statusError(405, (message, data, allow) => { + if (typeof allow === 'string') { + allow = [allow]; + } + + const headers = Array.isArray(allow) + ? { + Allow: allow.join(', '), + } + : undefined; + + return [message, { data, headers }]; +}); + +export const notAcceptable = statusError(406); + +export const proxyAuthRequired = statusError(407); + +export const clientTimeout = statusError(408); + +export const conflict = statusError(409); + +export const resourceGone = statusError(410); + +export const lengthRequired = statusError(411); + +export const preconditionFailed = statusError(412); + +export const entityTooLarge = statusError(413); + +export const uriTooLong = statusError(414); + +export const unsupportedMediaType = statusError(415); + +export const rangeNotSatisfiable = statusError(416); + +export const expectationFailed = statusError(417); + +export const teapot = statusError(418); + +export const badData = statusError(422); + +export const locked = statusError(423); + +export const failedDependency = statusError(424); + +export const tooEarly = statusError(425); + +export const preconditionRequired = statusError(428); + +export const tooManyRequests = statusError(429); + +export const illegal = statusError(451); + +// 5xx Server Errors + +function serverError(message, data) { + const isDataNonBoomError = data instanceof Error && !isBoom(data); + + return [message, isDataNonBoomError ? { cause: data } : { data }]; +} + +export const internal = statusError(500, (message, data, statusCode = 500) => { + const res = serverError(message, data); + if (statusCode !== 500) { + const [, options] = res; + options.statusCode = statusCode; + } + + return res; +}); + +export const notImplemented = statusError(501, serverError); + +export const badGateway = statusError(502, serverError); + +export const serverUnavailable = statusError(503, serverError); + +export const gatewayTimeout = statusError(504, serverError); + +export const badImplementation = statusError(500, (message, data) => { + return [...serverError(message, data), { isDeveloperError: true }]; +}); diff --git a/test/index.js b/test/index.js index 8c34257..1c760ce 100755 --- a/test/index.js +++ b/test/index.js @@ -1,41 +1,29 @@ -'use strict'; - -const Boom = require('..'); -const Boom10 = require('@hapi/boom10'); -const Code = require('@hapi/code'); -const Lab = require('@hapi/lab'); - - -const internals = {}; - - -const { describe, it } = exports.lab = Lab.script(); -const expect = Code.expect; +import * as Boom10 from '@hapi/boom10'; +import { describe, expect, it, onTestFinished } from 'vitest'; +import * as Boom from '../src/index.js'; describe('Boom', () => { - it('constructs error object (new)', () => { - const err = new Boom.Boom('oops', { statusCode: 400 }); - expect(err.output.payload.message).to.equal('oops'); - expect(err.output.statusCode).to.equal(400); - - expect(err.name).to.equal('Boom'); - expect(Object.keys(err)).to.equal(['data', 'output']); - expect(JSON.stringify(err)).to.equal('{"data":null,"output":{"statusCode":400,"payload":{"statusCode":400,"error":"Bad Request","message":"oops"},"headers":{}}}'); + expect(err.output.payload.message).toBe('oops'); + expect(err.output.statusCode).toBe(400); + + expect(err.name).toBe('Boom'); + expect(Object.keys(err)).toEqual(['data', 'output']); + expect(JSON.stringify(err)).toBe( + '{"data":null,"output":{"statusCode":400,"payload":{"statusCode":400,"error":"Bad Request","message":"oops"},"headers":{}}}', + ); }); it('instances has .name "Boom"', () => { - class SubBoom extends Boom.Boom {} - expect(new Boom.Boom().name).to.equal('Boom'); - expect(new SubBoom().name).to.equal('Boom'); + expect(new Boom.Boom().name).toBe('Boom'); + expect(new SubBoom().name).toBe('Boom'); }); it('instances .name can be changed', () => { - class SubBoom extends Boom.Boom { name = 'BadaBoom'; } @@ -43,1135 +31,967 @@ describe('Boom', () => { const err = new Boom.Boom(); err.name = 'MyBoom'; - expect(err.name).to.equal('MyBoom'); - expect(new SubBoom().name).to.equal('BadaBoom'); + expect(err.name).toBe('MyBoom'); + expect(new SubBoom().name).toBe('BadaBoom'); }); it('handles missing message', () => { - const err = new Boom.Boom(); - expect(Boom.isBoom(err)).to.be.true(); - expect(err.message).to.equal('Internal Server Error'); + expect(Boom.isBoom(err)).toBe(true); + expect(err.message).toBe('Internal Server Error'); }); it('handles missing message with unknown statusCode', () => { - const err = new Boom.Boom(null, { statusCode: 999 }); - expect(Boom.isBoom(err)).to.be.true(); - expect(err.message).to.equal('Unknown'); + expect(Boom.isBoom(err)).toBe(true); + expect(err.message).toBe('Unknown'); }); it('handles missing message (subclass)', () => { - const Example = class extends Boom.Boom {}; const err = new Example(); - expect(Boom.isBoom(err)).to.be.true(); + expect(Boom.isBoom(err)).toBe(true); }); it('handles headers option', () => { - const err = new Boom.Boom('fail', { statusCode: 400, headers: { custom: 'yes' } }); - expect(err.output.payload.message).to.equal('fail'); - expect(err.output.statusCode).to.equal(400); - expect(err.output.headers).to.equal({ custom: 'yes' }); + expect(err.output.payload.message).toBe('fail'); + expect(err.output.statusCode).toBe(400); + expect(err.output.headers).toEqual({ custom: 'yes' }); }); it('clones headers object', () => { - const headers = { custom: ['yes'] }; const err = new Boom.Boom('fail', { statusCode: 400, headers }); err.output.headers.custom.push('more'); err.output.headers.extra = 'added'; - expect(err.output.headers).to.equal({ custom: ['yes', 'more'], extra: 'added' }); - expect(headers).to.equal({ custom: ['yes'] }); + expect(err.output.headers).toEqual({ custom: ['yes', 'more'], extra: 'added' }); + expect(headers).toEqual({ custom: ['yes'] }); }); it('throws TypeError on non-object headers option', () => { - - expect(() => { - - new Boom.Boom('fail', { statusCode: 400, headers: true }); - }).to.throw(TypeError); - - expect(() => { - - new Boom.Boom('fail', { statusCode: 400, headers: null }); - }).to.throw(TypeError); + expect(() => new Boom.Boom('fail', { statusCode: 400, headers: true })).toThrow(TypeError); + expect(() => new Boom.Boom('fail', { statusCode: 400, headers: null })).toThrow(TypeError); }); it('throws when statusCode is invalid', () => { + expect(() => new Boom.Boom('message', { statusCode: 'x' })).toThrow('statusCode must be a number (400+): x'); - expect(() => { - - new Boom.Boom('message', { statusCode: 'x' }); - }).to.throw('statusCode must be a number (400+): x'); - - expect(() => { - - new Boom.Boom('message', { statusCode: '200' }); - }).to.throw('statusCode must be a number (400+): 200'); + expect(() => new Boom.Boom('message', { statusCode: '200' })).toThrow( + 'statusCode must be a number (400+): 200', + ); }); it('will cast a statusCode number-string to an integer', () => { - const codes = [ { input: '404', result: 404 }, { input: '404.1', result: 404 }, { input: 400, result: 400 }, - { input: 400.123, result: 400 } + { input: 400.123, result: 400 }, ]; - for (let i = 0; i < codes.length; ++i) { - const code = codes[i]; + for (const code of codes) { const err = new Boom.Boom('', { statusCode: code.input }); - expect(err.output.statusCode).to.equal(code.result); + expect(err.output.statusCode).toBe(code.result); } }); it('throws TypeError when statusCode is not finite', () => { + const fn = () => new Boom.Boom('', { statusCode: 1 / 0 }); - expect(() => { - - new Boom.Boom('', { statusCode: 1 / 0 }); - }).to.throw(TypeError, 'statusCode must be a number (400+): Infinity'); + expect(fn).toThrow(TypeError); + expect(fn).toThrow('statusCode must be a number (400+): Infinity'); }); it('sets error code to unknown', () => { - const err = new Boom.Boom('', { statusCode: 999 }); - expect(err.output.payload.error).to.equal('Unknown'); + expect(err.output.payload.error).toBe('Unknown'); }); it('only sets cause when part of options', () => { - const err1 = new Boom.Boom('fail', { cause: undefined }); - expect(err1).to.include('cause'); - expect(err1.cause).to.equal(undefined); + expect(Object.hasOwn(err1, 'cause')).toBe(true); + expect(err1.cause).toBeUndefined(); const err2 = new Boom.Boom('fail', {}); - expect(err2).to.not.include('cause'); - expect(err2.cause).to.equal(undefined); + expect(Object.hasOwn(err2, 'cause')).toBe(false); + expect(err2.cause).toBeUndefined(); }); - it('assigns a .cause property if Error does not support it', (flags) => { - + it('assigns a .cause property if Error does not support it', () => { const proto = Object.getPrototypeOf(Boom.Boom); - Object.setPrototypeOf(Boom.Boom, class extends Error { - - constructor(message, _options) { - - super(message); - } - }); - - flags.onCleanup = () => { + Object.setPrototypeOf( + Boom.Boom, + class extends Error { + constructor(message) { + super(message); + } + }, + ); + onTestFinished(() => { Object.setPrototypeOf(Boom.Boom, proto); - }; + }); const err = new Boom.Boom('fail', { cause: 0 }); - expect(err.cause).to.exist(); - expect(err.cause).to.equal(0); + expect(err.cause).toBe(0); }); describe('instanceof', () => { - it('identifies a boom object', () => { - const BadaBoom = class extends Boom.Boom {}; - expect(new Boom.Boom('oops')).to.be.instanceOf(Boom.Boom); - expect(new Boom10.Boom('oops')).to.be.instanceOf(Boom.Boom); - expect(new BadaBoom('oops')).to.be.instanceOf(Boom.Boom); - expect(Boom.badRequest('oops')).to.be.instanceOf(Boom.Boom); - expect(new Error('oops')).to.not.be.instanceOf(Boom.Boom); - expect({ isBoom: true }).to.not.be.instanceOf(Boom.Boom); - expect(null).to.not.be.instanceOf(Boom.Boom); + expect(new Boom.Boom('oops')).toBeInstanceOf(Boom.Boom); + expect(new Boom10.Boom('oops')).toBeInstanceOf(Boom.Boom); + expect(new BadaBoom('oops')).toBeInstanceOf(Boom.Boom); + expect(Boom.badRequest('oops')).toBeInstanceOf(Boom.Boom); + expect(new Error('oops')).not.toBeInstanceOf(Boom.Boom); + expect({ isBoom: true }).not.toBeInstanceOf(Boom.Boom); + expect(null).not.toBeInstanceOf(Boom.Boom); }); it('can be called on a sub-class', () => { - const BadaBoom = class extends Boom.Boom {}; // Success - expect(new BadaBoom('oops')).to.be.instanceOf(BadaBoom); - expect(Object.create(BadaBoom.prototype)).to.be.instanceOf(BadaBoom); + expect(new BadaBoom('oops')).toBeInstanceOf(BadaBoom); + expect(Object.create(BadaBoom.prototype)).toBeInstanceOf(BadaBoom); // Fail - expect(new Boom.Boom('oops')).to.not.be.instanceOf(BadaBoom); - expect(Boom.badRequest('oops')).to.not.be.instanceOf(BadaBoom); + expect(new Boom.Boom('oops')).not.toBeInstanceOf(BadaBoom); + expect(Boom.badRequest('oops')).not.toBeInstanceOf(BadaBoom); }); it('works from legacy boom', () => { - - expect(new Boom.Boom('oops')).to.be.instanceOf(Boom10.Boom); - expect(new Boom10.Boom('oops')).to.be.instanceOf(Boom10.Boom); + expect(new Boom.Boom('oops')).toBeInstanceOf(Boom10.Boom); + expect(new Boom10.Boom('oops')).toBeInstanceOf(Boom10.Boom); }); }); describe('isBoom()', () => { - it('identifies a boom object', () => { - // Success - expect(Boom.isBoom(new Boom.Boom('oops'))).to.be.true(); - expect(Boom.isBoom(new Boom10.Boom('oops'))).to.be.true(); + expect(Boom.isBoom(new Boom.Boom('oops'))).toBe(true); + expect(Boom.isBoom(new Boom10.Boom('oops'))).toBe(true); // Fail - expect(Boom.isBoom(new Error('oops'))).to.be.false(); - expect(Boom.isBoom({ isBoom: true })).to.be.false(); - expect(Boom.isBoom(null)).to.be.false(); + expect(Boom.isBoom(new Error('oops'))).toBe(false); + expect(Boom.isBoom({ isBoom: true })).toBe(false); + expect(Boom.isBoom(null)).toBe(false); }); it('returns true for valid boom object and valid status code', () => { - - expect(Boom.isBoom(Boom.notFound(),404)).to.be.true(); - expect(Boom.isBoom(Boom10.notFound(), 404)).to.be.true(); + expect(Boom.isBoom(Boom.notFound(), 404)).toBe(true); + expect(Boom.isBoom(Boom10.notFound(), 404)).toBe(true); }); it('returns false for valid boom object and wrong status code', () => { - - expect(Boom.isBoom(Boom.notFound(), 503)).to.be.false(); - expect(Boom.isBoom(Boom10.notFound(), 503)).to.be.false(); + expect(Boom.isBoom(Boom.notFound(), 503)).toBe(false); + expect(Boom.isBoom(Boom10.notFound(), 503)).toBe(false); }); it('works from legacy boom', () => { - - expect(Boom10.isBoom(new Boom.Boom('oops'))).to.be.true(); - expect(Boom10.isBoom(new Boom10.Boom('oops'))).to.be.true(); + expect(Boom10.isBoom(new Boom.Boom('oops'))).toBe(true); + expect(Boom10.isBoom(new Boom10.Boom('oops'))).toBe(true); }); }); describe('boomify()', () => { - it('returns the same object when already boom', () => { - const error = Boom.badRequest(); - expect(error).to.shallow.equal(Boom.boomify(error)); - expect(error).to.shallow.equal(Boom.boomify(error, { statusCode: 444 })); + expect(Boom.boomify(error)).toBe(error); + expect(Boom.boomify(error, { statusCode: 444 })).toBe(error); }); it('returns an error with info when constructed using another error', () => { - const error = new Error('ka-boom'); const err = Boom.boomify(error); - expect(err.cause).to.shallow.equal(error); - expect(err.output).to.equal({ + expect(err.cause).toBe(error); + expect(err.output).toEqual({ statusCode: 500, payload: { statusCode: 500, error: 'Internal Server Error', - message: 'An internal server error occurred' + message: 'An internal server error occurred', }, - headers: {} + headers: {}, }); - expect(err.data).to.equal(null); + expect(err.data).toBeNull(); }); it('sets new message when none exists', () => { - const error = new Error(); const wrapped = Boom.boomify(error, { statusCode: 400, message: 'something bad' }); - expect(wrapped.message).to.equal('something bad'); + expect(wrapped.message).toBe('something bad'); }); it('returns boom error unchanged', () => { - const error = Boom.badRequest('Missing data', { type: 'user' }); const boom = Boom.boomify(error); - expect(boom).to.shallow.equal(error); - expect(error.data.type).to.equal('user'); - expect(error.output.payload.message).to.equal('Missing data'); - expect(error.output.statusCode).to.equal(400); + expect(boom).toBe(error); + expect(error.data.type).toBe('user'); + expect(error.output.payload.message).toBe('Missing data'); + expect(error.output.statusCode).toBe(400); }); it('defaults to 500', () => { - const error = new Error('Missing data'); const boom = Boom.boomify(error); - expect(boom.cause).to.shallow.equal(error); - expect(boom.output.payload.message).to.equal('An internal server error occurred'); - expect(boom.output.statusCode).to.equal(500); + expect(boom.cause).toBe(error); + expect(boom.output.payload.message).toBe('An internal server error occurred'); + expect(boom.output.statusCode).toBe(500); }); it('overrides message and statusCode', () => { - const error = Boom.badRequest('Missing data', { type: 'user' }); const boom = Boom.boomify(error, { message: 'Override message', statusCode: 599 }); - expect(boom).to.shallow.equal(error); - expect(error.data.type).to.equal('user'); - expect(error.output.payload.message).to.equal('Override message: Missing data'); - expect(error.output.statusCode).to.equal(599); + expect(boom).toBe(error); + expect(error.data.type).toBe('user'); + expect(error.output.payload.message).toBe('Override message: Missing data'); + expect(error.output.statusCode).toBe(599); }); it('overrides message', () => { - const error = Boom.badRequest('Missing data', { type: 'user' }); const boom = Boom.boomify(error, { message: 'Override message' }); - expect(boom).to.shallow.equal(error); - expect(error.data.type).to.equal('user'); - expect(error.output.payload.message).to.equal('Override message: Missing data'); - expect(error.output.statusCode).to.equal(400); + expect(boom).toBe(error); + expect(error.data.type).toBe('user'); + expect(error.output.payload.message).toBe('Override message: Missing data'); + expect(error.output.statusCode).toBe(400); }); it('overrides statusCode', () => { - const error = Boom.badRequest('Missing data', { type: 'user' }); const boom = Boom.boomify(error, { statusCode: 599 }); - expect(boom).to.shallow.equal(error); - expect(error.data.type).to.equal('user'); - expect(error.output.payload.message).to.equal('Missing data'); - expect(error.output.statusCode).to.equal(599); + expect(boom).toBe(error); + expect(error.data.type).toBe('user'); + expect(error.output.payload.message).toBe('Missing data'); + expect(error.output.statusCode).toBe(599); }); it('skips override', () => { - const error = Boom.badRequest('Missing data', { type: 'user' }); - const boom = Boom.boomify(error, { message: 'Override message', statusCode: 599, override: false }); + const boom = Boom.boomify(error, { + message: 'Override message', + statusCode: 599, + override: false, + }); - expect(boom).to.shallow.equal(error); - expect(error.data.type).to.equal('user'); - expect(error.output.payload.message).to.equal('Missing data'); - expect(error.output.statusCode).to.equal(400); + expect(boom).toBe(error); + expect(error.data.type).toBe('user'); + expect(error.output.payload.message).toBe('Missing data'); + expect(error.output.statusCode).toBe(400); }); it('initializes plain error', () => { - const error = new Error('Missing data'); - const boom = Boom.boomify(error, { message: 'Override message', statusCode: 599, override: false }); + const boom = Boom.boomify(error, { + message: 'Override message', + statusCode: 599, + override: false, + }); - expect(boom.cause).to.shallow.equal(error); - expect(boom.output.payload.message).to.equal('Override message: Missing data'); - expect(boom.output.statusCode).to.equal(599); + expect(boom.cause).toBe(error); + expect(boom.output.payload.message).toBe('Override message: Missing data'); + expect(boom.output.statusCode).toBe(599); }); it('handles non-Error errors', () => { - const boom = Boom.boomify(123, { message: 'Hello', statusCode: 400 }); - expect(boom.cause).to.equal(123); - expect(boom.output.payload.message).to.equal('Hello: 123'); - expect(boom.output.statusCode).to.equal(400); + expect(boom.cause).toBe(123); + expect(boom.output.payload.message).toBe('Hello: 123'); + expect(boom.output.statusCode).toBe(400); }); it('only sets isServer when it is an own property', () => { - const boom = Boom.boomify(new Boom.Boom()); - expect(boom.isServer).to.be.true(); - expect(boom.hasOwnProperty('isServer')).to.be.false(); + expect(boom.isServer).toBe(true); + expect(Object.hasOwn(boom, 'isServer')).toBe(false); const Boom2 = class extends Boom.Boom { - isServer = undefined; }; const boom2 = Boom.boomify(new Boom2()); - expect(boom2.isServer).to.be.true(); - expect(boom2.hasOwnProperty('isServer')).to.be.true(); + expect(boom2.isServer).toBe(true); + expect(Object.hasOwn(boom2, 'isServer')).toBe(true); }); it('works with legacy boom', () => { + const boom = Boom.boomify(new Boom10.Boom(null, { statusCode: 404 }), { + statusCode: 501, + message: 'Override', + }); - const boom = Boom.boomify(new Boom10.Boom(null, { statusCode: 404 }), { statusCode: 501, message: 'Override' }); - - expect(boom.cause).to.be.undefined(); - expect(boom.message).to.equal('Override: Not Found'); - expect(boom.isServer).to.be.true(); - expect(boom.hasOwnProperty('isServer')).to.be.true(); - expect(boom.output.statusCode).to.equal(501); + expect(boom.cause).toBeUndefined(); + expect(boom.message).toBe('Override: Not Found'); + expect(boom.isServer).toBe(true); + expect(Object.hasOwn(boom, 'isServer')).toBe(true); + expect(boom.output.statusCode).toBe(501); - const boom10 = Boom10.boomify(new Boom.Boom(null, { statusCode: 404 }), { statusCode: 501, message: 'Override' }); + const boom10 = Boom10.boomify(new Boom.Boom(null, { statusCode: 404 }), { + statusCode: 501, + message: 'Override', + }); - expect(boom10.cause).to.be.undefined(); - expect(boom10.message).to.equal('Override: Not Found'); - expect(boom10.isServer).to.be.true(); - expect(boom10.hasOwnProperty('isServer')).to.be.true(); - expect(boom10.output.statusCode).to.equal(501); + expect(boom10.cause).toBeUndefined(); + expect(boom10.message).toBe('Override: Not Found'); + expect(boom10.isServer).toBe(true); + expect(Object.hasOwn(boom10, 'isServer')).toBe(true); + expect(boom10.output.statusCode).toBe(501); }); }); describe('create()', () => { - it('does not set null message', () => { - const error = Boom.unauthorized(null); - expect(error.output.payload.message).to.equal('Unauthorized'); - expect(error.isServer).to.be.false(); + expect(error.output.payload.message).toBe('Unauthorized'); + expect(error.isServer).toBe(false); }); it('sets message and data', () => { - const error = Boom.badRequest('Missing data', { type: 'user' }); - expect(error.data.type).to.equal('user'); - expect(error.output.payload.message).to.equal('Missing data'); + expect(error.data.type).toBe('user'); + expect(error.output.payload.message).toBe('Missing data'); }); }); describe('initialize()', () => { - it('does not set null message', () => { - const err = new Error('some error'); const boom = new Boom.Boom('prepended error message', { statusCode: 400, cause: err }); - expect(boom.output.payload.message).to.equal('prepended error message: some error'); + expect(boom.output.payload.message).toBe('prepended error message: some error'); }); }); describe('isBoom', () => { - it('is true for Boom object', () => { - - expect(Boom.badRequest().isBoom).to.be.true(); + expect(Boom.badRequest().isBoom).toBe(true); }); }); - const utilities = ['badRequest', 'unauthorized', 'forbidden', 'notFound', 'methodNotAllowed', - 'notAcceptable', 'proxyAuthRequired', 'clientTimeout', 'conflict', - 'resourceGone', 'lengthRequired', 'preconditionFailed', 'entityTooLarge', - 'uriTooLong', 'unsupportedMediaType', 'rangeNotSatisfiable', 'expectationFailed', - 'badData', 'preconditionRequired', 'tooManyRequests', + const utilities = [ + 'badRequest', + 'unauthorized', + 'forbidden', + 'notFound', + 'methodNotAllowed', + 'notAcceptable', + 'proxyAuthRequired', + 'clientTimeout', + 'conflict', + 'resourceGone', + 'lengthRequired', + 'preconditionFailed', + 'entityTooLarge', + 'uriTooLong', + 'unsupportedMediaType', + 'rangeNotSatisfiable', + 'expectationFailed', + 'badData', + 'preconditionRequired', + 'tooManyRequests', // 500s - 'internal', 'notImplemented', 'badGateway', 'serverUnavailable', - 'gatewayTimeout', 'badImplementation' + 'internal', + 'notImplemented', + 'badGateway', + 'serverUnavailable', + 'gatewayTimeout', + 'badImplementation', ]; describe('badRequest()', () => { - it('returns a 400 error statusCode', () => { - const error = Boom.badRequest(); - expect(error.output.statusCode).to.equal(400); - expect(error.isServer).to.be.false(); + expect(error.output.statusCode).toBe(400); + expect(error.isServer).toBe(false); }); it('sets the message with the passed in message', () => { - - expect(Boom.badRequest('my message').message).to.equal('my message'); + expect(Boom.badRequest('my message').message).toBe('my message'); }); it('sets the message to HTTP status if none provided', () => { - - expect(Boom.badRequest().message).to.equal('Bad Request'); + expect(Boom.badRequest().message).toBe('Bad Request'); }); }); describe('unauthorized()', () => { - it('returns a 401 error statusCode', () => { - const err = Boom.unauthorized(); - expect(err.output.statusCode).to.equal(401); - expect(err.output.headers).to.equal({}); + expect(err.output.statusCode).toBe(401); + expect(err.output.headers).toEqual({}); }); it('sets the message with the passed in message', () => { - - expect(Boom.unauthorized('my message').message).to.equal('my message'); + expect(Boom.unauthorized('my message').message).toBe('my message'); }); it('returns a WWW-Authenticate header when passed a scheme', () => { - const err = Boom.unauthorized('boom', 'Test'); - expect(err.output.statusCode).to.equal(401); - expect(err.output.headers['WWW-Authenticate']).to.equal('Test error="boom"'); + expect(err.output.statusCode).toBe(401); + expect(err.output.headers['WWW-Authenticate']).toBe('Test error="boom"'); }); it('returns a WWW-Authenticate header when passed a scheme (no message)', () => { - const err = Boom.unauthorized(null, 'Test'); - expect(err.output.statusCode).to.equal(401); - expect(err.output.headers['WWW-Authenticate']).to.equal('Test'); + expect(err.output.statusCode).toBe(401); + expect(err.output.headers['WWW-Authenticate']).toBe('Test'); }); it('returns a WWW-Authenticate header set to the schema array value', () => { - const err = Boom.unauthorized(null, ['Test', 'one', 'two']); - expect(err.output.statusCode).to.equal(401); - expect(err.output.headers['WWW-Authenticate']).to.equal('Test, one, two'); + expect(err.output.statusCode).toBe(401); + expect(err.output.headers['WWW-Authenticate']).toBe('Test, one, two'); }); it('returns a WWW-Authenticate header when passed a scheme and attributes', () => { - const err = Boom.unauthorized('boom', 'Test', { a: 1, b: 'something', c: null, d: 0 }); - expect(err.output.statusCode).to.equal(401); - expect(err.output.headers['WWW-Authenticate']).to.equal('Test a="1", b="something", c="", d="0", error="boom"'); + expect(err.output.statusCode).toBe(401); + expect(err.output.headers['WWW-Authenticate']).toBe('Test a="1", b="something", c="", d="0", error="boom"'); }); it('returns a WWW-Authenticate header when passed a scheme and empty attributes', () => { - const err = Boom.unauthorized('boom', 'Test', {}); - expect(err.output.statusCode).to.equal(401); - expect(err.output.headers['WWW-Authenticate']).to.equal('Test error="boom"'); + expect(err.output.statusCode).toBe(401); + expect(err.output.headers['WWW-Authenticate']).toBe('Test error="boom"'); }); it('returns a WWW-Authenticate header from string input instead of object', () => { - const err = Boom.unauthorized(null, 'Negotiate', 'VGhpcyBpcyBhIHRlc3QgdG9rZW4='); - expect(err.output.statusCode).to.equal(401); - expect(err.output.headers['WWW-Authenticate']).to.equal('Negotiate VGhpcyBpcyBhIHRlc3QgdG9rZW4='); + expect(err.output.statusCode).toBe(401); + expect(err.output.headers['WWW-Authenticate']).toBe('Negotiate VGhpcyBpcyBhIHRlc3QgdG9rZW4='); }); it('returns a WWW-Authenticate header when passed attributes, missing error', () => { - - const err = Boom.unauthorized(null, 'Test', { a: 1, b: 'something', c: null, d: 0, e: undefined }); - expect(err.output.statusCode).to.equal(401); - expect(err.output.headers['WWW-Authenticate']).to.equal('Test a="1", b="something", c="", d="0", e=""'); - expect(err.isMissing).to.equal(true); + const err = Boom.unauthorized(null, 'Test', { + a: 1, + b: 'something', + c: null, + d: 0, + e: undefined, + }); + expect(err.output.statusCode).toBe(401); + expect(err.output.headers['WWW-Authenticate']).toBe('Test a="1", b="something", c="", d="0", e=""'); + expect(err.isMissing).toBe(true); }); it('sets the isMissing flag when error message is empty', () => { - const err = Boom.unauthorized('', 'Basic'); - expect(err.isMissing).to.equal(true); + expect(err.isMissing).toBe(true); }); it('does not set the isMissing flag when error message is not empty', () => { - const err = Boom.unauthorized('message', 'Basic'); - expect(err.isMissing).to.equal(undefined); + expect(err.isMissing).toBeUndefined(); }); it('sets a WWW-Authenticate when passed as an array', () => { - const err = Boom.unauthorized('message', ['Basic', 'Example e="1"', 'Another x="3", y="4"']); - expect(err.output.headers['WWW-Authenticate']).to.equal('Basic, Example e="1", Another x="3", y="4"'); + expect(err.output.headers['WWW-Authenticate']).toBe('Basic, Example e="1", Another x="3", y="4"'); }); }); - describe('paymentRequired()', () => { - it('returns a 402 error statusCode', () => { - - expect(Boom.paymentRequired().output.statusCode).to.equal(402); + expect(Boom.paymentRequired().output.statusCode).toBe(402); }); it('sets the message with the passed in message', () => { - - expect(Boom.paymentRequired('my message').message).to.equal('my message'); + expect(Boom.paymentRequired('my message').message).toBe('my message'); }); it('sets the message to HTTP status if none provided', () => { - - expect(Boom.paymentRequired().message).to.equal('Payment Required'); + expect(Boom.paymentRequired().message).toBe('Payment Required'); }); }); - describe('methodNotAllowed()', () => { - it('returns a 405 error statusCode', () => { - - expect(Boom.methodNotAllowed().output.statusCode).to.equal(405); + expect(Boom.methodNotAllowed().output.statusCode).toBe(405); }); it('sets the message with the passed in message', () => { - - expect(Boom.methodNotAllowed('my message').message).to.equal('my message'); + expect(Boom.methodNotAllowed('my message').message).toBe('my message'); }); it('returns an Allow header when passed a string', () => { - const err = Boom.methodNotAllowed('my message', null, 'GET'); - expect(err.output.statusCode).to.equal(405); - expect(err.output.headers.Allow).to.equal('GET'); + expect(err.output.statusCode).toBe(405); + expect(err.output.headers.Allow).toBe('GET'); }); it('returns an Allow header when passed an array', () => { - const err = Boom.methodNotAllowed('my message', null, ['GET', 'POST']); - expect(err.output.statusCode).to.equal(405); - expect(err.output.headers.Allow).to.equal('GET, POST'); + expect(err.output.statusCode).toBe(405); + expect(err.output.headers.Allow).toBe('GET, POST'); }); }); - describe('notAcceptable()', () => { - it('returns a 406 error statusCode', () => { - - expect(Boom.notAcceptable().output.statusCode).to.equal(406); + expect(Boom.notAcceptable().output.statusCode).toBe(406); }); it('sets the message with the passed in message', () => { - - expect(Boom.notAcceptable('my message').message).to.equal('my message'); + expect(Boom.notAcceptable('my message').message).toBe('my message'); }); }); - describe('proxyAuthRequired()', () => { - it('returns a 407 error statusCode', () => { - - expect(Boom.proxyAuthRequired().output.statusCode).to.equal(407); + expect(Boom.proxyAuthRequired().output.statusCode).toBe(407); }); it('sets the message with the passed in message', () => { - - expect(Boom.proxyAuthRequired('my message').message).to.equal('my message'); + expect(Boom.proxyAuthRequired('my message').message).toBe('my message'); }); }); - describe('clientTimeout()', () => { - it('returns a 408 error statusCode', () => { - - expect(Boom.clientTimeout().output.statusCode).to.equal(408); + expect(Boom.clientTimeout().output.statusCode).toBe(408); }); it('sets the message with the passed in message', () => { - - expect(Boom.clientTimeout('my message').message).to.equal('my message'); + expect(Boom.clientTimeout('my message').message).toBe('my message'); }); }); - describe('conflict()', () => { - it('returns a 409 error statusCode', () => { - - expect(Boom.conflict().output.statusCode).to.equal(409); + expect(Boom.conflict().output.statusCode).toBe(409); }); it('sets the message with the passed in message', () => { - - expect(Boom.conflict('my message').message).to.equal('my message'); + expect(Boom.conflict('my message').message).toBe('my message'); }); }); - describe('resourceGone()', () => { - it('returns a 410 error statusCode', () => { - - expect(Boom.resourceGone().output.statusCode).to.equal(410); + expect(Boom.resourceGone().output.statusCode).toBe(410); }); it('sets the message with the passed in message', () => { - - expect(Boom.resourceGone('my message').message).to.equal('my message'); + expect(Boom.resourceGone('my message').message).toBe('my message'); }); }); - describe('lengthRequired()', () => { - it('returns a 411 error statusCode', () => { - - expect(Boom.lengthRequired().output.statusCode).to.equal(411); + expect(Boom.lengthRequired().output.statusCode).toBe(411); }); it('sets the message with the passed in message', () => { - - expect(Boom.lengthRequired('my message').message).to.equal('my message'); + expect(Boom.lengthRequired('my message').message).toBe('my message'); }); }); - describe('preconditionFailed()', () => { - it('returns a 412 error statusCode', () => { - - expect(Boom.preconditionFailed().output.statusCode).to.equal(412); + expect(Boom.preconditionFailed().output.statusCode).toBe(412); }); it('sets the message with the passed in message', () => { - - expect(Boom.preconditionFailed('my message').message).to.equal('my message'); + expect(Boom.preconditionFailed('my message').message).toBe('my message'); }); }); - describe('entityTooLarge()', () => { - it('returns a 413 error statusCode', () => { - - expect(Boom.entityTooLarge().output.statusCode).to.equal(413); + expect(Boom.entityTooLarge().output.statusCode).toBe(413); }); it('sets the message with the passed in message', () => { - - expect(Boom.entityTooLarge('my message').message).to.equal('my message'); + expect(Boom.entityTooLarge('my message').message).toBe('my message'); }); }); - describe('uriTooLong()', () => { - it('returns a 414 error statusCode', () => { - - expect(Boom.uriTooLong().output.statusCode).to.equal(414); + expect(Boom.uriTooLong().output.statusCode).toBe(414); }); it('sets the message with the passed in message', () => { - - expect(Boom.uriTooLong('my message').message).to.equal('my message'); + expect(Boom.uriTooLong('my message').message).toBe('my message'); }); }); - describe('unsupportedMediaType()', () => { - it('returns a 415 error statusCode', () => { - - expect(Boom.unsupportedMediaType().output.statusCode).to.equal(415); + expect(Boom.unsupportedMediaType().output.statusCode).toBe(415); }); it('sets the message with the passed in message', () => { - - expect(Boom.unsupportedMediaType('my message').message).to.equal('my message'); + expect(Boom.unsupportedMediaType('my message').message).toBe('my message'); }); }); - describe('rangeNotSatisfiable()', () => { - it('returns a 416 error statusCode', () => { - - expect(Boom.rangeNotSatisfiable().output.statusCode).to.equal(416); + expect(Boom.rangeNotSatisfiable().output.statusCode).toBe(416); }); it('sets the message with the passed in message', () => { - - expect(Boom.rangeNotSatisfiable('my message').message).to.equal('my message'); + expect(Boom.rangeNotSatisfiable('my message').message).toBe('my message'); }); }); - describe('expectationFailed()', () => { - it('returns a 417 error statusCode', () => { - - expect(Boom.expectationFailed().output.statusCode).to.equal(417); + expect(Boom.expectationFailed().output.statusCode).toBe(417); }); it('sets the message with the passed in message', () => { - - expect(Boom.expectationFailed('my message').message).to.equal('my message'); + expect(Boom.expectationFailed('my message').message).toBe('my message'); }); }); - describe('teapot()', () => { - it('returns a 418 error statusCode', () => { - - expect(Boom.teapot().output.statusCode).to.equal(418); + expect(Boom.teapot().output.statusCode).toBe(418); }); it('sets the message with the passed in message', () => { - - expect(Boom.teapot('Sorry, no coffee...').message).to.equal('Sorry, no coffee...'); + expect(Boom.teapot('Sorry, no coffee...').message).toBe('Sorry, no coffee...'); }); }); - describe('badData()', () => { - it('returns a 422 error statusCode', () => { - - expect(Boom.badData().output.statusCode).to.equal(422); + expect(Boom.badData().output.statusCode).toBe(422); }); it('sets the message with the passed in message', () => { - - expect(Boom.badData('my message').message).to.equal('my message'); + expect(Boom.badData('my message').message).toBe('my message'); }); }); - describe('locked()', () => { - it('returns a 423 error statusCode', () => { - - expect(Boom.locked().output.statusCode).to.equal(423); + expect(Boom.locked().output.statusCode).toBe(423); }); it('sets the message with the passed in message', () => { - - expect(Boom.locked('my message').message).to.equal('my message'); + expect(Boom.locked('my message').message).toBe('my message'); }); }); describe('failedDependency()', () => { - it('returns a 424 error statusCode', () => { - - expect(Boom.failedDependency().output.statusCode).to.equal(424); + expect(Boom.failedDependency().output.statusCode).toBe(424); }); it('sets the message with the passed in message', () => { - - expect(Boom.failedDependency('my message').message).to.equal('my message'); + expect(Boom.failedDependency('my message').message).toBe('my message'); }); }); describe('tooEarly()', () => { - it('returns a 425 error statusCode', () => { - - expect(Boom.tooEarly().output.statusCode).to.equal(425); + expect(Boom.tooEarly().output.statusCode).toBe(425); }); it('sets the message with the passed in message', () => { - - expect(Boom.tooEarly('my message').message).to.equal('my message'); + expect(Boom.tooEarly('my message').message).toBe('my message'); }); }); - describe('preconditionRequired()', () => { - it('returns a 428 error statusCode', () => { - - expect(Boom.preconditionRequired().output.statusCode).to.equal(428); + expect(Boom.preconditionRequired().output.statusCode).toBe(428); }); it('sets the message with the passed in message', () => { - - expect(Boom.preconditionRequired('my message').message).to.equal('my message'); + expect(Boom.preconditionRequired('my message').message).toBe('my message'); }); }); - describe('tooManyRequests()', () => { - it('returns a 429 error statusCode', () => { - - expect(Boom.tooManyRequests().output.statusCode).to.equal(429); + expect(Boom.tooManyRequests().output.statusCode).toBe(429); }); it('sets the message with the passed-in message', () => { - - expect(Boom.tooManyRequests('my message').message).to.equal('my message'); + expect(Boom.tooManyRequests('my message').message).toBe('my message'); }); }); - describe('illegal()', () => { - it('returns a 451 error statusCode', () => { - - expect(Boom.illegal().output.statusCode).to.equal(451); + expect(Boom.illegal().output.statusCode).toBe(451); }); it('sets the message with the passed-in message', () => { - - expect(Boom.illegal('my message').message).to.equal('my message'); + expect(Boom.illegal('my message').message).toBe('my message'); }); }); describe('serverUnavailable()', () => { - it('returns a 503 error statusCode', () => { - - expect(Boom.serverUnavailable().output.statusCode).to.equal(503); + expect(Boom.serverUnavailable().output.statusCode).toBe(503); }); it('sets the message with the passed in message', () => { - - expect(Boom.serverUnavailable('my message').message).to.equal('my message'); + expect(Boom.serverUnavailable('my message').message).toBe('my message'); }); }); describe('forbidden()', () => { - it('returns a 403 error statusCode', () => { - - expect(Boom.forbidden().output.statusCode).to.equal(403); + expect(Boom.forbidden().output.statusCode).toBe(403); }); it('sets the message with the passed in message', () => { - - expect(Boom.forbidden('my message').message).to.equal('my message'); + expect(Boom.forbidden('my message').message).toBe('my message'); }); }); describe('notFound()', () => { - it('returns a 404 error statusCode', () => { - - expect(Boom.notFound().output.statusCode).to.equal(404); + expect(Boom.notFound().output.statusCode).toBe(404); }); it('sets the message with the passed in message', () => { - - expect(Boom.notFound('my message').message).to.equal('my message'); + expect(Boom.notFound('my message').message).toBe('my message'); }); }); describe('internal()', () => { - it('returns a 500 error statusCode', () => { - - expect(Boom.internal().output.statusCode).to.equal(500); + expect(Boom.internal().output.statusCode).toBe(500); }); it('handles a custom error statusCode', () => { - const err = Boom.internal(null, null, 507); - expect(err.output.statusCode).to.equal(507); - expect(err.message).to.equal('Insufficient Storage'); + expect(err.output.statusCode).toBe(507); + expect(err.message).toBe('Insufficient Storage'); }); it('sets the message with the passed in message', () => { - const err = Boom.internal('my message'); - expect(err.message).to.equal('my message'); - expect(err.isServer).to.true(); - expect(err.output.payload.message).to.equal('An internal server error occurred'); + expect(err.message).toBe('my message'); + expect(err.isServer).toBe(true); + expect(err.output.payload.message).toBe('An internal server error occurred'); }); it('passes data on the callback if its passed in', () => { - - expect(Boom.internal('my message', { my: 'data' }).data.my).to.equal('data'); + expect(Boom.internal('my message', { my: 'data' }).data.my).toBe('data'); }); it('uses data with Error as cause', () => { - const insideErr = new Error('inside'); const err = Boom.internal('my message', insideErr); - expect(err.data).to.not.exist(); - expect(err.cause).to.shallow.equal(insideErr); + expect(err.data).toBeNull(); + expect(err.cause).toBe(insideErr); }); it('returns an error with composite message', () => { - const x = {}; try { x.foo(); - } - catch (err) { + } catch (err) { const boom = Boom.internal('Something bad', err); boom.reformat(true); - expect(boom.message).to.equal('Something bad'); - expect(boom.cause).to.be.an.error(TypeError, 'x.foo is not a function'); - expect(boom.output.payload.message).to.equal('Something bad: x.foo is not a function'); - expect(boom.isServer).to.be.true(); + expect(boom.message).toBe('Something bad'); + expect(boom.cause).toBeInstanceOf(TypeError); + expect(boom.cause.message).toBe('x.foo is not a function'); + expect(boom.output.payload.message).toBe('Something bad: x.foo is not a function'); + expect(boom.isServer).toBe(true); } }); }); describe('notImplemented()', () => { - it('returns a 501 error statusCode', () => { - - expect(Boom.notImplemented().output.statusCode).to.equal(501); + expect(Boom.notImplemented().output.statusCode).toBe(501); }); it('sets the message with the passed in message', () => { - - expect(Boom.notImplemented('my message').message).to.equal('my message'); + expect(Boom.notImplemented('my message').message).toBe('my message'); }); it('uses data with Error as cause', () => { - const insideErr = new Error('inside'); const err = Boom.notImplemented('my message', insideErr); - expect(err.data).to.not.exist(); - expect(err.cause).to.shallow.equal(insideErr); + expect(err.data).toBeNull(); + expect(err.cause).toBe(insideErr); }); }); describe('badGateway()', () => { - it('returns a 502 error statusCode', () => { - - expect(Boom.badGateway().output.statusCode).to.equal(502); + expect(Boom.badGateway().output.statusCode).toBe(502); }); it('sets the message with the passed in message', () => { - - expect(Boom.badGateway('my message').message).to.equal('my message'); + expect(Boom.badGateway('my message').message).toBe('my message'); }); it('retains source boom error as data when wrapped', () => { - const upstream = Boom.serverUnavailable(); const boom = Boom.badGateway('Upstream error', upstream); - expect(boom.output.statusCode).to.equal(502); - expect(boom.data).to.equal(upstream); + expect(boom.output.statusCode).toBe(502); + expect(boom.data).toBe(upstream); }); it('uses data with Error as cause', () => { - const insideErr = new Error('inside'); const err = Boom.badGateway('my message', insideErr); - expect(err.data).to.not.exist(); - expect(err.cause).to.shallow.equal(insideErr); + expect(err.data).toBeNull(); + expect(err.cause).toBe(insideErr); }); }); describe('gatewayTimeout()', () => { - it('returns a 504 error statusCode', () => { - - expect(Boom.gatewayTimeout().output.statusCode).to.equal(504); + expect(Boom.gatewayTimeout().output.statusCode).toBe(504); }); it('sets the message with the passed in message', () => { - - expect(Boom.gatewayTimeout('my message').message).to.equal('my message'); + expect(Boom.gatewayTimeout('my message').message).toBe('my message'); }); it('uses data with Error as cause', () => { - const insideErr = new Error('inside'); const err = Boom.gatewayTimeout('my message', insideErr); - expect(err.data).to.not.exist(); - expect(err.cause).to.shallow.equal(insideErr); + expect(err.data).toBeNull(); + expect(err.cause).toBe(insideErr); }); }); describe('badImplementation()', () => { - it('returns a 500 error statusCode', () => { - const err = Boom.badImplementation(); - expect(err.output.statusCode).to.equal(500); - expect(err.isDeveloperError).to.equal(true); - expect(err.isServer).to.be.true(); + expect(err.output.statusCode).toBe(500); + expect(err.isDeveloperError).toBe(true); + expect(err.isServer).toBe(true); }); it('hides error from user when error data is included', () => { - const err = Boom.badImplementation('Invalid', new Error('kaboom')); - expect(err.output).to.equal({ + expect(err.output).toEqual({ headers: {}, statusCode: 500, payload: { error: 'Internal Server Error', message: 'An internal server error occurred', - statusCode: 500 - } + statusCode: 500, + }, }); }); it('hides error from user when error data is included (boom)', () => { - const err = Boom.badImplementation('Invalid', Boom.badRequest('kaboom')); - expect(err.isDeveloperError).to.equal(true); - expect(err.output).to.equal({ + expect(err.isDeveloperError).toBe(true); + expect(err.output).toEqual({ headers: {}, statusCode: 500, payload: { error: 'Internal Server Error', message: 'An internal server error occurred', - statusCode: 500 - } + statusCode: 500, + }, }); }); it('uses data with Error as cause', () => { - const insideErr = new Error('inside'); const err = Boom.badImplementation('my message', insideErr); - expect(err.data).to.not.exist(); - expect(err.cause).to.shallow.equal(insideErr); + expect(err.data).toBeNull(); + expect(err.cause).toBe(insideErr); }); }); describe('stack trace', () => { - - it('should omit lib', () => { - + it('should omit src', () => { for (const name of utilities) { const err = Boom[name](); - expect(err.stack).to.not.match(/(\/|\\)lib(\/|\\)index\.js/); + expect(err.stack).not.toMatch(/(\/|\\)src(\/|\\)index\.js/); } }); - it('should not crash when Error.captureStackTrace is missing', (flags) => { - + it('should not crash when Error.captureStackTrace is missing', () => { const captureStackTrace = Error.captureStackTrace; for (const name of utilities) { + let err; + try { Error.captureStackTrace = undefined; - var err = Boom[name](); - } - finally { + err = Boom[name](); + } finally { Error.captureStackTrace = captureStackTrace; } - expect(err.stack).to.match(/(\/|\\)lib(\/|\\)index\.js/); + expect(err.stack).toMatch(/(\/|\\)src(\/|\\)index\.js/); } }); }); describe('method with error object instead of message', () => { - - for (const name of utilities) { - it(`uses stringified error as message`, () => { - - const error = new Error('An example mongoose validation error'); - error.name = 'ValidationError'; - const err = Boom[name](error); - expect(err.cause).to.not.exist(); - expect(err.message).to.equal(error.toString()); - }); - } + it.each(utilities)('%s uses stringified error as message', (name) => { + const error = new Error('An example mongoose validation error'); + error.name = 'ValidationError'; + const err = Boom[name](error); + expect(err.cause).toBeUndefined(); + expect(err.message).toBe(error.toString()); + }); }); describe('reformat()', () => { - it('displays internal server error messages in debug mode', () => { - const error = new Error('ka-boom'); const err = new Boom.Boom(null, { statusCode: 500, cause: error }); err.reformat(false); - expect(err.output).to.equal({ + expect(err.output).toEqual({ statusCode: 500, payload: { statusCode: 500, error: 'Internal Server Error', - message: 'An internal server error occurred' + message: 'An internal server error occurred', }, - headers: {} + headers: {}, }); err.reformat(true); - expect(err.output).to.equal({ + expect(err.output).toEqual({ statusCode: 500, payload: { statusCode: 500, error: 'Internal Server Error', - message: 'ka-boom' + message: 'ka-boom', }, - headers: {} + headers: {}, }); }); it('is redefinable', () => { - Object.defineProperty(new Boom.Boom('oops'), 'reformat', { value: true }); }); it('can be implemented by subclasses to apply custom formatting', () => { - class MyBoom extends Boom.Boom { - reformat(...args) { - super.reformat(...args); this.output.payload.custom = true; @@ -1179,31 +999,29 @@ describe('Boom', () => { } const err = new MyBoom('boom', { statusCode: 400 }); - expect(err.output.statusCode).to.equal(400); - expect(err.output.payload.message).to.equal('boom'); - expect(err.output.payload.custom).to.be.true(); + expect(err.output.statusCode).toBe(400); + expect(err.output.payload.message).toBe('boom'); + expect(err.output.payload.custom).toBe(true); err.output.statusCode = 500; err.reformat(); - expect(err.output.statusCode).to.equal(500); - expect(err.output.payload.message).to.equal('An internal server error occurred'); - expect(err.output.payload.custom).to.be.true(); + expect(err.output.statusCode).toBe(500); + expect(err.output.payload.message).toBe('An internal server error occurred'); + expect(err.output.payload.custom).toBe(true); }); - it('prototype can be changed to always debug', (flags) => { - + it('prototype can be changed to always debug', () => { const proto = Boom.Boom.prototype.reformat; - flags.onCleanup = () => { + onTestFinished(() => { Boom.Boom.prototype.reformat = proto; - }; + }); Boom.Boom.prototype.reformat = function () { - return proto.call(this, true); }; - expect(Boom.internal('DEBUG').output.payload.message).to.equal('DEBUG'); + expect(Boom.internal('DEBUG').output.payload.message).toBe('DEBUG'); }); }); }); diff --git a/test/index.ts b/test/index.ts deleted file mode 100755 index fdeac2c..0000000 --- a/test/index.ts +++ /dev/null @@ -1,447 +0,0 @@ -import * as Boom from '..'; -import * as Lab from '@hapi/lab'; - -const { expect } = Lab.types; - - -// new Boom.Boom() - -expect.type(new Boom.Boom()); -expect.type>(new Boom.Boom('error')); -expect.type>(new Boom.Boom('error', { data: true })); -expect.type>(new Boom.Boom('error')); -expect.type>(new Boom.Boom('error', { data: true })); - -expect.error(new Boom.Boom(null)); -expect.error(new Boom.Boom(new Error('error'))); -expect.error(new Boom.Boom('error')); -expect.error(new Boom.Boom('error', { data: true })); - - -class CustomError extends Boom.Boom {} -expect.type(new CustomError('Some error')); - - -const boom = new Boom.Boom('some error'); -expect.type(boom.output); -boom.output.payload.custom_null = null; -boom.output.payload.custom_number = 42; -boom.output.payload.custom_string = 'foo'; -boom.output.payload.custom_boolean = true; -boom.output.payload.custom_object = { bar: 42 }; -boom.output.headers['header1'] = 'foo'; -boom.output.headers['header2'] = ['foo', 'bar']; -boom.output.headers['header3'] = 42; -boom.output.headers['header4'] = undefined; -expect.type(boom.output.payload); -expect.type(boom.output.headers); - - -// boomify() - -const error = new Error('Unexpected input'); -class BadaBoom extends Boom.Boom { - constructor() { super('boom', { data: 1 }) } -} - -expect.type(Boom.boomify(error)); -expect.type(Boom.boomify(error, { statusCode: 400 })); -expect.type(Boom.boomify(error, { statusCode: 400, message: 'Unexpected Input', override: false })); -expect.type(Boom.boomify('error')); -expect.type>(Boom.boomify(new Boom.Boom<{ foo: 'bar' }>('error', { data: { foo: 'bar' }}))); -expect.type>(Boom.boomify(error, { data: 'bla' })); -expect.type>(Boom.boomify>(error, { data: 'bla' })); -expect.type(Boom.boomify(new BadaBoom(), { statusCode: 400 })); -expect.type>(Boom.boomify(new BadaBoom(), { statusCode: 400 })); -expect.type(Boom.boomify(new BadaBoom(), { data: 'bla' }).data); - -expect.type>(Boom.boomify(new Boom.Boom('error', { data: 'ok' }))); -expect.type>(Boom.boomify(new Boom.Boom('error', { data: 'ok' }), { data: 1 })); -expect.type>(Boom.boomify>(new Boom.Boom('error', { data: 'ok' }), { data: 1 })); - -expect.error(Boom.boomify()); -expect.error(Boom.boomify(error, { statusCode: '400' })); -expect.error(Boom.boomify(error, { statusCode: 400, message: true })); -expect.error(Boom.boomify(error, { statusCode: 400, override: 'false' })); -expect.error(Boom.boomify(error, { decorate: { x: 'y' } })); -//expect.error(Boom.boomify>(new Boom.Boom('error', { data: 'ok' }))); // Cannot work without partial type inference (https://github.com/microsoft/TypeScript/issues/26242) - -// isBoom - -expect.type(Boom.boomify(error).isBoom); - -// isBoom() - -expect.type(Boom.isBoom(error)); -expect.type(Boom.isBoom(error, 404)); -expect.type(Boom.isBoom(Boom.boomify(error))); -expect.type(Boom.isBoom('error')); -expect.type(Boom.isBoom({ foo: 'bar' })); -expect.type(Boom.isBoom({ error: true })); - -expect.error(Boom.isBoom(error, 'test')); -expect.error(Boom.isBoom()); - - -// 4xx Errors - -// badRequest() - -expect.type>(Boom.badRequest('invalid query', 'some data')); -expect.type>(Boom.badRequest('invalid query', { foo: 'bar' })); -expect.type(Boom.badRequest('invalid query')); -expect.type(Boom.badRequest()); - -expect.error(Boom.badRequest(400)); -expect.error(Boom.badRequest({ foo: 'bar' })); -expect.error(Boom.badRequest(new Error())); - - -// unauthorized() - -expect.type>(Boom.unauthorized('invalid password')); -expect.type>(Boom.unauthorized('invalid password', 'simple')); -expect.type>(Boom.unauthorized(null, 'Negotiate', 'VGhpcyBpcyBhIHRlc3QgdG9rZW4=')); -expect.type>(Boom.unauthorized('invalid password', 'sample', { ttl: 0, cache: null, foo: 'bar' })); -expect.type>(Boom.unauthorized('invalid password', 'sample', { ttl: 0, cache: null, foo: 'bar' } as Boom.unauthorized.Attributes)); -expect.type>(Boom.unauthorized()); -expect.type>(Boom.unauthorized('basic', ['a', 'b', 'c'])); -expect.type & Boom.unauthorized.MissingAuth>(Boom.unauthorized('', 'basic')); -expect.type & Boom.unauthorized.MissingAuth>(Boom.unauthorized(null, 'basic')); -expect.type(Boom.unauthorized('', 'basic').isMissing); -expect.type(Boom.unauthorized(null, 'basic').isMissing); - -expect.error(Boom.unauthorized(401)); -expect.error(Boom.unauthorized('invalid password', 500)); -expect.error(Boom.unauthorized('invalid password', 'sample', 500)); -expect.error(Boom.unauthorized('basic', ['a', 'b', 'c'], 'test')); -expect.error(Boom.unauthorized('message', 'basic').isMissing); - -// paymentRequired() - -expect.type>(Boom.paymentRequired('bandwidth used', 'some data')); -expect.type>(Boom.paymentRequired('bandwidth used', { foo: 'bar' })); -expect.type(Boom.paymentRequired('bandwidth used')); -expect.type(Boom.paymentRequired()); - -expect.error(Boom.paymentRequired(402)); -expect.error(Boom.paymentRequired({ foo: 'bar' })); - - -// forbidden() - -expect.type>(Boom.forbidden('try again some time', 'some data')); -expect.type>(Boom.forbidden('try again some time', { foo: 'bar' })); -expect.type(Boom.forbidden('try again some time')); -expect.type(Boom.forbidden()); - -expect.error(Boom.forbidden(403)); -expect.error(Boom.forbidden({ foo: 'bar' })); - - -// notFound() - -expect.type>(Boom.notFound('missing', 'some data')); -expect.type>(Boom.notFound('missing', { foo: 'bar' })); -expect.type(Boom.notFound('missing')); -expect.type(Boom.notFound()); - -expect.error(Boom.notFound(404)); -expect.error(Boom.notFound({ foo: 'bar' })); - - -// methodNotAllowed() - -expect.type>(Boom.methodNotAllowed('this method is not allowed', 'some data')); -expect.type>(Boom.methodNotAllowed('this method is not allowed', { foo: 'bar' })); -expect.type(Boom.methodNotAllowed('this method is not allowed')); -expect.type(Boom.methodNotAllowed()); - -expect.error(Boom.methodNotAllowed(405)); -expect.error(Boom.methodNotAllowed({ foo: 'bar' })); - - -// notAcceptable() - -expect.type>(Boom.notAcceptable('unacceptable', 'some data')); -expect.type>(Boom.notAcceptable('unacceptable', { foo: 'bar' })); -expect.type(Boom.notAcceptable('unacceptable')); -expect.type(Boom.notAcceptable()); - -expect.error(Boom.notAcceptable(406)); -expect.error(Boom.notAcceptable({ foo: 'bar' })); - - -// proxyAuthRequired() - -expect.type>(Boom.proxyAuthRequired('auth missing', 'some data')); -expect.type>(Boom.proxyAuthRequired('auth missing', { foo: 'bar' })); -expect.type(Boom.proxyAuthRequired('auth missing')); -expect.type(Boom.proxyAuthRequired()); - -expect.error(Boom.proxyAuthRequired(407)); -expect.error(Boom.proxyAuthRequired({ foo: 'bar' })); - - -// clientTimeout() - -expect.type>(Boom.clientTimeout('timed out', 'some data')); -expect.type>(Boom.clientTimeout('timed out', { foo: 'bar' })); -expect.type(Boom.clientTimeout('timed out')); -expect.type(Boom.clientTimeout()); - -expect.error(Boom.clientTimeout(408)); -expect.error(Boom.clientTimeout({ foo: 'bar' })); - - -// conflict() - -expect.type>(Boom.conflict('there was a conflict', 'some data')); -expect.type>(Boom.conflict('there was a conflict', { foo: 'bar' })); -expect.type(Boom.conflict('there was a conflict')); -expect.type(Boom.conflict()); - -expect.error(Boom.conflict(409)); -expect.error(Boom.conflict({ foo: 'bar' })); - - -// resourceGone() - -expect.type>(Boom.resourceGone('it is gone', 'some data')); -expect.type>(Boom.resourceGone('it is gone', { foo: 'bar' })); -expect.type(Boom.resourceGone('it is gone')); -expect.type(Boom.resourceGone()); - -expect.error(Boom.resourceGone(410)); -expect.error(Boom.resourceGone({ foo: 'bar' })); - - -// lengthRequired() - -expect.type>(Boom.lengthRequired('length needed', 'some data')); -expect.type>(Boom.lengthRequired('length needed', { foo: 'bar' })); -expect.type(Boom.lengthRequired('length needed')); -expect.type(Boom.lengthRequired()); - -expect.error(Boom.lengthRequired(411)); -expect.error(Boom.lengthRequired({ foo: 'bar' })); - - -// preconditionFailed() - -expect.type>(Boom.preconditionFailed('failed', 'some data')); -expect.type>(Boom.preconditionFailed('failed', { foo: 'bar' })); -expect.type(Boom.preconditionFailed('failed')); -expect.type(Boom.preconditionFailed()); - -expect.error(Boom.preconditionFailed(412)); -expect.error(Boom.preconditionFailed({ foo: 'bar' })); - - -// entityTooLarge() - -expect.type>(Boom.entityTooLarge('too big', 'some data')); -expect.type>(Boom.entityTooLarge('too big', { foo: 'bar' })); -expect.type(Boom.entityTooLarge('too big')); -expect.type(Boom.entityTooLarge()); - -expect.error(Boom.entityTooLarge(413)); -expect.error(Boom.entityTooLarge({ foo: 'bar' })); - - -// uriTooLong() - -expect.type>(Boom.uriTooLong('uri is too long', 'some data')); -expect.type>(Boom.uriTooLong('uri is too long', { foo: 'bar' })); -expect.type(Boom.uriTooLong('uri is too long')); -expect.type(Boom.uriTooLong()); - -expect.error(Boom.uriTooLong(414)); -expect.error(Boom.uriTooLong({ foo: 'bar' })); - - -// unsupportedMediaType() - -expect.type>(Boom.unsupportedMediaType('that media is not supported', 'some data')); -expect.type>(Boom.unsupportedMediaType('that media is not supported', { foo: 'bar' })); -expect.type(Boom.unsupportedMediaType('that media is not supported')); -expect.type(Boom.unsupportedMediaType()); - -expect.error(Boom.unsupportedMediaType(415)); -expect.error(Boom.unsupportedMediaType({ foo: 'bar' })); - - -// rangeNotSatisfiable() - -expect.type>(Boom.rangeNotSatisfiable('range not satisfiable', 'some data')); -expect.type>(Boom.rangeNotSatisfiable('range not satisfiable', { foo: 'bar' })); -expect.type(Boom.rangeNotSatisfiable('range not satisfiable')); -expect.type(Boom.rangeNotSatisfiable()); - -expect.error(Boom.rangeNotSatisfiable(416)); -expect.error(Boom.rangeNotSatisfiable({ foo: 'bar' })); - - -// expectationFailed() - -expect.type>(Boom.expectationFailed('expected this to work', 'some data')); -expect.type>(Boom.expectationFailed('expected this to work', { foo: 'bar' })); -expect.type(Boom.expectationFailed('expected this to work')); -expect.type(Boom.expectationFailed()); - -expect.error(Boom.expectationFailed(417)); -expect.error(Boom.expectationFailed({ foo: 'bar' })); - - -// teapot() - -expect.type>(Boom.teapot('sorry, no coffee...', 'some data')); -expect.type>(Boom.teapot('sorry, no coffee...', { foo: 'bar' })); -expect.type(Boom.teapot('sorry, no coffee...')); -expect.type(Boom.teapot()); - -expect.error(Boom.teapot(418)); -expect.error(Boom.teapot({ foo: 'bar' })); - - -// badData() - -expect.type>(Boom.badData('your data is bad and you should feel bad', 'some data')); -expect.type>(Boom.badData('your data is bad and you should feel bad', { foo: 'bar' })); -expect.type(Boom.badData('your data is bad and you should feel bad')); -expect.type(Boom.badData()); - -expect.error(Boom.badData(422)); -expect.error(Boom.badData({ foo: 'bar' })); - - -// locked() - -expect.type>(Boom.locked('this resource has been locked', 'some data')); -expect.type>(Boom.locked('this resource has been locked', { foo: 'bar' })); -expect.type(Boom.locked('this resource has been locked')); -expect.type(Boom.locked()); - -expect.error(Boom.locked(423)); -expect.error(Boom.locked({ foo: 'bar' })); - - -// failedDependency() - -expect.type>(Boom.failedDependency('an external resource failed', 'some data')); -expect.type>(Boom.failedDependency('an external resource failed', { foo: 'bar' })); -expect.type(Boom.failedDependency('an external resource failed')); -expect.type(Boom.failedDependency()); - -expect.error(Boom.failedDependency(424)); -expect.error(Boom.failedDependency({ foo: 'bar' })); - -// tooEarly() - -expect.type>(Boom.tooEarly('won\'t process your request', 'some data')); -expect.type>(Boom.tooEarly('won\'t process your request', { foo: 'bar' })); -expect.type(Boom.tooEarly('won\'t process your request')); -expect.type(Boom.tooEarly()); - -expect.error(Boom.tooEarly(425)); -expect.error(Boom.tooEarly({ foo: 'bar' })); - -// preconditionRequired() - -expect.type>(Boom.preconditionRequired('you must supple an If-Match header', 'some data')); -expect.type>(Boom.preconditionRequired('you must supple an If-Match header', { foo: 'bar' })); -expect.type(Boom.preconditionRequired('you must supple an If-Match header')); -expect.type(Boom.preconditionRequired()); - -expect.error(Boom.preconditionRequired(428)); -expect.error(Boom.preconditionRequired({ foo: 'bar' })); - - -// tooManyRequests() - -expect.type>(Boom.tooManyRequests('you have exceeded your request limit', 'some data')); -expect.type>(Boom.tooManyRequests('you have exceeded your request limit', { foo: 'bar' })); -expect.type(Boom.tooManyRequests('you have exceeded your request limit')); -expect.type(Boom.tooManyRequests()); - -expect.error(Boom.tooManyRequests(414)); -expect.error(Boom.tooManyRequests({ foo: 'bar' })); - - -// illegal() - -expect.type>(Boom.illegal('you are not permitted to view this resource for legal reasons', 'some data')); -expect.type>(Boom.illegal('you are not permitted to view this resource for legal reasons', { foo: 'bar' })); -expect.type(Boom.illegal('you are not permitted to view this resource for legal reasons')); -expect.type(Boom.illegal()); - -expect.error(Boom.illegal(451)); -expect.error(Boom.illegal({ foo: 'bar' })); - - -// 5xx Errors - -// internal() - -expect.type>(Boom.internal('terrible implementation', 'some data', 599)); -expect.type>(Boom.internal('terrible implementation', { foo: 'bar' })); -expect.type(Boom.internal('terrible implementation')); -expect.type(Boom.internal()); - -expect.error(Boom.internal(500)); -expect.error(Boom.internal({ foo: 'bar' })); - - -// badImplementation() - -expect.type>(Boom.badImplementation('terrible implementation', 'some data')); -expect.type>(Boom.badImplementation('terrible implementation', { foo: 'bar' })); -expect.type(Boom.badImplementation('terrible implementation')); -expect.type(Boom.badImplementation()); - -expect.error(Boom.badImplementation(500)); -expect.error(Boom.badImplementation({ foo: 'bar' })); - - -// notImplemented() - -expect.type>(Boom.notImplemented('method not implemented', 'some data')); -expect.type>(Boom.notImplemented('method not implemented', { foo: 'bar' })); -expect.type(Boom.notImplemented('method not implemented')); -expect.type(Boom.notImplemented()); - -expect.error(Boom.notImplemented(501)); -expect.error(Boom.notImplemented({ foo: 'bar' })); - - -// badGateway() - -expect.type>(Boom.badGateway('this is a bad gateway', 'some data')); -expect.type>(Boom.badGateway('this is a bad gateway', { foo: 'bar' })); -expect.type(Boom.badGateway('this is a bad gateway')); -expect.type(Boom.badGateway()); - -expect.error(Boom.badGateway(502)); -expect.error(Boom.badGateway({ foo: 'bar' })); - - -// serverUnavailable() - -expect.type>(Boom.serverUnavailable('unavailable', 'some data')); -expect.type>(Boom.serverUnavailable('unavailable', { foo: 'bar' })); -expect.type(Boom.serverUnavailable('unavailable')); -expect.type(Boom.serverUnavailable()); - -expect.error(Boom.serverUnavailable(503)); -expect.error(Boom.serverUnavailable({ foo: 'bar' })); - - -// gatewayTimeout() - -expect.type>(Boom.gatewayTimeout('gateway timeout', 'some data')); -expect.type>(Boom.gatewayTimeout('gateway timeout', { foo: 'bar' })); -expect.type(Boom.gatewayTimeout('gateway timeout')); -expect.type(Boom.gatewayTimeout()); - -expect.error(Boom.gatewayTimeout(504)); -expect.error(Boom.gatewayTimeout({ foo: 'bar' })); diff --git a/test/typings.ts b/test/typings.ts new file mode 100644 index 0000000..9a1ecc9 --- /dev/null +++ b/test/typings.ts @@ -0,0 +1,709 @@ +import { describe, expectTypeOf, it } from 'vitest'; + +import * as Boom from '../src/index.js'; + +describe('typings', () => { + describe('Boom', () => { + it('accepts valid calls', () => { + expectTypeOf(new Boom.Boom()).toExtend(); + expectTypeOf(new Boom.Boom('error')).toExtend>(); + expectTypeOf(new Boom.Boom('error', { data: true })).toExtend>(); + expectTypeOf(new Boom.Boom('error')).toExtend>(); + expectTypeOf(new Boom.Boom('error', { data: true })).toExtend>(); + + class CustomError extends Boom.Boom {} + expectTypeOf(new CustomError('Some error')).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + new Boom.Boom(null); + // @ts-expect-error message must be a string + new Boom.Boom(new Error('error')); + // @ts-expect-error data is required when Data is set + new Boom.Boom('error'); + // @ts-expect-error data must match Data + new Boom.Boom('error', { data: true }); + }); + + it('exposes a writable output', () => { + const boom = new Boom.Boom('some error'); + expectTypeOf(boom.output).toEqualTypeOf(); + + boom.output.payload.custom_null = null; + boom.output.payload.custom_number = 42; + boom.output.payload.custom_string = 'foo'; + boom.output.payload.custom_boolean = true; + boom.output.payload.custom_object = { bar: 42 }; + boom.output.headers['header1'] = 'foo'; + boom.output.headers['header2'] = ['foo', 'bar']; + boom.output.headers['header3'] = 42; + boom.output.headers['header4'] = undefined; + + expectTypeOf(boom.output.payload).toExtend(); + expectTypeOf(boom.output.headers).toEqualTypeOf(); + }); + }); + + describe('boomify()', () => { + const error = new Error('Unexpected input'); + + class BadaBoom extends Boom.Boom { + constructor() { + super('boom', { data: 1 }); + } + } + + it('accepts valid calls', () => { + expectTypeOf(Boom.boomify(error)).toExtend(); + expectTypeOf(Boom.boomify(error, { statusCode: 400 })).toExtend(); + expectTypeOf( + Boom.boomify(error, { statusCode: 400, message: 'Unexpected Input', override: false }), + ).toExtend(); + expectTypeOf(Boom.boomify('error')).toExtend(); + expectTypeOf(Boom.boomify(new Boom.Boom<{ foo: 'bar' }>('error', { data: { foo: 'bar' } }))).toExtend< + Boom.Boom<{ foo: 'bar' }> + >(); + expectTypeOf(Boom.boomify(error, { data: 'bla' })).toExtend>(); + expectTypeOf(Boom.boomify>(error, { data: 'bla' })).toExtend>(); + expectTypeOf(Boom.boomify(new BadaBoom(), { statusCode: 400 })).toExtend(); + expectTypeOf(Boom.boomify(new BadaBoom(), { statusCode: 400 })).toExtend>(); + expectTypeOf(Boom.boomify(new BadaBoom(), { data: 'bla' }).data).toEqualTypeOf(); + + expectTypeOf(Boom.boomify(new Boom.Boom('error', { data: 'ok' }))).toExtend>(); + expectTypeOf(Boom.boomify(new Boom.Boom('error', { data: 'ok' }), { data: 1 })).toExtend< + Boom.Boom + >(); + expectTypeOf( + Boom.boomify>(new Boom.Boom('error', { data: 'ok' }), { data: 1 }), + ).toExtend>(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error requires an error + Boom.boomify(); + // @ts-expect-error statusCode must be a number + Boom.boomify(error, { statusCode: '400' }); + // @ts-expect-error message must be a string + Boom.boomify(error, { statusCode: 400, message: true }); + // @ts-expect-error override must be a boolean + Boom.boomify(error, { statusCode: 400, override: 'false' }); + // @ts-expect-error unknown option + Boom.boomify(error, { decorate: { x: 'y' } }); + + // Cannot work without partial type inference (https://github.com/microsoft/TypeScript/issues/26242) + // Boom.boomify>(new Boom.Boom('error', { data: 'ok' })); + }); + }); + + describe('isBoom()', () => { + const error = new Error('Unexpected input'); + + it('accepts valid calls', () => { + expectTypeOf(Boom.boomify(error).isBoom).toEqualTypeOf(); + + expectTypeOf(Boom.isBoom(error)).toBeBoolean(); + expectTypeOf(Boom.isBoom(error, 404)).toBeBoolean(); + expectTypeOf(Boom.isBoom(Boom.boomify(error))).toBeBoolean(); + expectTypeOf(Boom.isBoom('error')).toBeBoolean(); + expectTypeOf(Boom.isBoom({ foo: 'bar' })).toBeBoolean(); + expectTypeOf(Boom.isBoom({ error: true })).toBeBoolean(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error statusCode must be a number + Boom.isBoom(error, 'test'); + // @ts-expect-error requires an object + Boom.isBoom(); + }); + }); + + // 4xx Errors + + describe('badRequest()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.badRequest('invalid query', 'some data')).toExtend>(); + expectTypeOf(Boom.badRequest('invalid query', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.badRequest('invalid query')).toExtend(); + expectTypeOf(Boom.badRequest()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.badRequest(400); + // @ts-expect-error message must be a string + Boom.badRequest({ foo: 'bar' }); + // @ts-expect-error message must be a string + Boom.badRequest(new Error()); + }); + }); + + describe('unauthorized()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.unauthorized('invalid password')).toExtend>(); + expectTypeOf(Boom.unauthorized('invalid password', 'simple')).toExtend>(); + expectTypeOf(Boom.unauthorized(null, 'Negotiate', 'VGhpcyBpcyBhIHRlc3QgdG9rZW4=')).toExtend< + Boom.Boom + >(); + expectTypeOf(Boom.unauthorized('invalid password', 'sample', { ttl: 0, cache: null, foo: 'bar' })).toExtend< + Boom.Boom + >(); + expectTypeOf( + Boom.unauthorized('invalid password', 'sample', { + ttl: 0, + cache: null, + foo: 'bar', + } as Boom.unauthorized.Attributes), + ).toExtend>(); + expectTypeOf(Boom.unauthorized()).toExtend>(); + expectTypeOf(Boom.unauthorized('basic', ['a', 'b', 'c'])).toExtend>(); + expectTypeOf(Boom.unauthorized('', 'basic')).toExtend & Boom.unauthorized.MissingAuth>(); + expectTypeOf(Boom.unauthorized(null, 'basic')).toExtend & Boom.unauthorized.MissingAuth>(); + expectTypeOf(Boom.unauthorized('', 'basic').isMissing).toEqualTypeOf(); + expectTypeOf(Boom.unauthorized(null, 'basic').isMissing).toEqualTypeOf(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.unauthorized(401); + try { + // @ts-expect-error scheme must be a string or an array + Boom.unauthorized('invalid password', 500); + } catch {} + // @ts-expect-error attributes must be a string or an object + Boom.unauthorized('invalid password', 'sample', 500); + // @ts-expect-error attributes are not supported with an array of schemes + Boom.unauthorized('basic', ['a', 'b', 'c'], 'test'); + // @ts-expect-error isMissing is only set when the message is empty + Boom.unauthorized('message', 'basic').isMissing; + }); + }); + + describe('paymentRequired()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.paymentRequired('bandwidth used', 'some data')).toExtend>(); + expectTypeOf(Boom.paymentRequired('bandwidth used', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.paymentRequired('bandwidth used')).toExtend(); + expectTypeOf(Boom.paymentRequired()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.paymentRequired(402); + // @ts-expect-error message must be a string + Boom.paymentRequired({ foo: 'bar' }); + }); + }); + + describe('forbidden()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.forbidden('try again some time', 'some data')).toExtend>(); + expectTypeOf(Boom.forbidden('try again some time', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.forbidden('try again some time')).toExtend(); + expectTypeOf(Boom.forbidden()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.forbidden(403); + // @ts-expect-error message must be a string + Boom.forbidden({ foo: 'bar' }); + }); + }); + + describe('notFound()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.notFound('missing', 'some data')).toExtend>(); + expectTypeOf(Boom.notFound('missing', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.notFound('missing')).toExtend(); + expectTypeOf(Boom.notFound()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.notFound(404); + // @ts-expect-error message must be a string + Boom.notFound({ foo: 'bar' }); + }); + }); + + describe('methodNotAllowed()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.methodNotAllowed('this method is not allowed', 'some data')).toExtend< + Boom.Boom + >(); + expectTypeOf(Boom.methodNotAllowed('this method is not allowed', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.methodNotAllowed('this method is not allowed')).toExtend(); + expectTypeOf(Boom.methodNotAllowed()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.methodNotAllowed(405); + // @ts-expect-error message must be a string + Boom.methodNotAllowed({ foo: 'bar' }); + }); + }); + + describe('notAcceptable()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.notAcceptable('unacceptable', 'some data')).toExtend>(); + expectTypeOf(Boom.notAcceptable('unacceptable', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.notAcceptable('unacceptable')).toExtend(); + expectTypeOf(Boom.notAcceptable()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.notAcceptable(406); + // @ts-expect-error message must be a string + Boom.notAcceptable({ foo: 'bar' }); + }); + }); + + describe('proxyAuthRequired()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.proxyAuthRequired('auth missing', 'some data')).toExtend>(); + expectTypeOf(Boom.proxyAuthRequired('auth missing', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.proxyAuthRequired('auth missing')).toExtend(); + expectTypeOf(Boom.proxyAuthRequired()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.proxyAuthRequired(407); + // @ts-expect-error message must be a string + Boom.proxyAuthRequired({ foo: 'bar' }); + }); + }); + + describe('clientTimeout()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.clientTimeout('timed out', 'some data')).toExtend>(); + expectTypeOf(Boom.clientTimeout('timed out', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.clientTimeout('timed out')).toExtend(); + expectTypeOf(Boom.clientTimeout()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.clientTimeout(408); + // @ts-expect-error message must be a string + Boom.clientTimeout({ foo: 'bar' }); + }); + }); + + describe('conflict()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.conflict('there was a conflict', 'some data')).toExtend>(); + expectTypeOf(Boom.conflict('there was a conflict', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.conflict('there was a conflict')).toExtend(); + expectTypeOf(Boom.conflict()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.conflict(409); + // @ts-expect-error message must be a string + Boom.conflict({ foo: 'bar' }); + }); + }); + + describe('resourceGone()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.resourceGone('it is gone', 'some data')).toExtend>(); + expectTypeOf(Boom.resourceGone('it is gone', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.resourceGone('it is gone')).toExtend(); + expectTypeOf(Boom.resourceGone()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.resourceGone(410); + // @ts-expect-error message must be a string + Boom.resourceGone({ foo: 'bar' }); + }); + }); + + describe('lengthRequired()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.lengthRequired('length needed', 'some data')).toExtend>(); + expectTypeOf(Boom.lengthRequired('length needed', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.lengthRequired('length needed')).toExtend(); + expectTypeOf(Boom.lengthRequired()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.lengthRequired(411); + // @ts-expect-error message must be a string + Boom.lengthRequired({ foo: 'bar' }); + }); + }); + + describe('preconditionFailed()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.preconditionFailed('failed', 'some data')).toExtend>(); + expectTypeOf(Boom.preconditionFailed('failed', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.preconditionFailed('failed')).toExtend(); + expectTypeOf(Boom.preconditionFailed()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.preconditionFailed(412); + // @ts-expect-error message must be a string + Boom.preconditionFailed({ foo: 'bar' }); + }); + }); + + describe('entityTooLarge()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.entityTooLarge('too big', 'some data')).toExtend>(); + expectTypeOf(Boom.entityTooLarge('too big', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.entityTooLarge('too big')).toExtend(); + expectTypeOf(Boom.entityTooLarge()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.entityTooLarge(413); + // @ts-expect-error message must be a string + Boom.entityTooLarge({ foo: 'bar' }); + }); + }); + + describe('uriTooLong()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.uriTooLong('uri is too long', 'some data')).toExtend>(); + expectTypeOf(Boom.uriTooLong('uri is too long', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.uriTooLong('uri is too long')).toExtend(); + expectTypeOf(Boom.uriTooLong()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.uriTooLong(414); + // @ts-expect-error message must be a string + Boom.uriTooLong({ foo: 'bar' }); + }); + }); + + describe('unsupportedMediaType()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.unsupportedMediaType('that media is not supported', 'some data')).toExtend< + Boom.Boom + >(); + expectTypeOf(Boom.unsupportedMediaType('that media is not supported', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.unsupportedMediaType('that media is not supported')).toExtend(); + expectTypeOf(Boom.unsupportedMediaType()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.unsupportedMediaType(415); + // @ts-expect-error message must be a string + Boom.unsupportedMediaType({ foo: 'bar' }); + }); + }); + + describe('rangeNotSatisfiable()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.rangeNotSatisfiable('range not satisfiable', 'some data')).toExtend>(); + expectTypeOf(Boom.rangeNotSatisfiable('range not satisfiable', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.rangeNotSatisfiable('range not satisfiable')).toExtend(); + expectTypeOf(Boom.rangeNotSatisfiable()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.rangeNotSatisfiable(416); + // @ts-expect-error message must be a string + Boom.rangeNotSatisfiable({ foo: 'bar' }); + }); + }); + + describe('expectationFailed()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.expectationFailed('expected this to work', 'some data')).toExtend>(); + expectTypeOf(Boom.expectationFailed('expected this to work', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.expectationFailed('expected this to work')).toExtend(); + expectTypeOf(Boom.expectationFailed()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.expectationFailed(417); + // @ts-expect-error message must be a string + Boom.expectationFailed({ foo: 'bar' }); + }); + }); + + describe('teapot()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.teapot('sorry, no coffee...', 'some data')).toExtend>(); + expectTypeOf(Boom.teapot('sorry, no coffee...', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.teapot('sorry, no coffee...')).toExtend(); + expectTypeOf(Boom.teapot()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.teapot(418); + // @ts-expect-error message must be a string + Boom.teapot({ foo: 'bar' }); + }); + }); + + describe('badData()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.badData('your data is bad and you should feel bad', 'some data')).toExtend< + Boom.Boom + >(); + expectTypeOf(Boom.badData('your data is bad and you should feel bad', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.badData('your data is bad and you should feel bad')).toExtend(); + expectTypeOf(Boom.badData()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.badData(422); + // @ts-expect-error message must be a string + Boom.badData({ foo: 'bar' }); + }); + }); + + describe('locked()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.locked('this resource has been locked', 'some data')).toExtend>(); + expectTypeOf(Boom.locked('this resource has been locked', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.locked('this resource has been locked')).toExtend(); + expectTypeOf(Boom.locked()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.locked(423); + // @ts-expect-error message must be a string + Boom.locked({ foo: 'bar' }); + }); + }); + + describe('failedDependency()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.failedDependency('an external resource failed', 'some data')).toExtend< + Boom.Boom + >(); + expectTypeOf(Boom.failedDependency('an external resource failed', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.failedDependency('an external resource failed')).toExtend(); + expectTypeOf(Boom.failedDependency()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.failedDependency(424); + // @ts-expect-error message must be a string + Boom.failedDependency({ foo: 'bar' }); + }); + }); + + describe('tooEarly()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.tooEarly("won't process your request", 'some data')).toExtend>(); + expectTypeOf(Boom.tooEarly("won't process your request", { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.tooEarly("won't process your request")).toExtend(); + expectTypeOf(Boom.tooEarly()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.tooEarly(425); + // @ts-expect-error message must be a string + Boom.tooEarly({ foo: 'bar' }); + }); + }); + + describe('preconditionRequired()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.preconditionRequired('you must supple an If-Match header', 'some data')).toExtend< + Boom.Boom + >(); + expectTypeOf(Boom.preconditionRequired('you must supple an If-Match header', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.preconditionRequired('you must supple an If-Match header')).toExtend(); + expectTypeOf(Boom.preconditionRequired()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.preconditionRequired(428); + // @ts-expect-error message must be a string + Boom.preconditionRequired({ foo: 'bar' }); + }); + }); + + describe('tooManyRequests()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.tooManyRequests('you have exceeded your request limit', 'some data')).toExtend< + Boom.Boom + >(); + expectTypeOf(Boom.tooManyRequests('you have exceeded your request limit', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.tooManyRequests('you have exceeded your request limit')).toExtend(); + expectTypeOf(Boom.tooManyRequests()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.tooManyRequests(414); + // @ts-expect-error message must be a string + Boom.tooManyRequests({ foo: 'bar' }); + }); + }); + + describe('illegal()', () => { + it('accepts valid calls', () => { + expectTypeOf( + Boom.illegal('you are not permitted to view this resource for legal reasons', 'some data'), + ).toExtend>(); + expectTypeOf( + Boom.illegal('you are not permitted to view this resource for legal reasons', { foo: 'bar' }), + ).toExtend>(); + expectTypeOf( + Boom.illegal('you are not permitted to view this resource for legal reasons'), + ).toExtend(); + expectTypeOf(Boom.illegal()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.illegal(451); + // @ts-expect-error message must be a string + Boom.illegal({ foo: 'bar' }); + }); + }); + + // 5xx Errors + + describe('internal()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.internal('terrible implementation', 'some data', 599)).toExtend>(); + expectTypeOf(Boom.internal('terrible implementation', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.internal('terrible implementation')).toExtend(); + expectTypeOf(Boom.internal()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.internal(500); + // @ts-expect-error message must be a string + Boom.internal({ foo: 'bar' }); + }); + }); + + describe('badImplementation()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.badImplementation('terrible implementation', 'some data')).toExtend>(); + expectTypeOf(Boom.badImplementation('terrible implementation', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.badImplementation('terrible implementation')).toExtend(); + expectTypeOf(Boom.badImplementation()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.badImplementation(500); + // @ts-expect-error message must be a string + Boom.badImplementation({ foo: 'bar' }); + }); + }); + + describe('notImplemented()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.notImplemented('method not implemented', 'some data')).toExtend>(); + expectTypeOf(Boom.notImplemented('method not implemented', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.notImplemented('method not implemented')).toExtend(); + expectTypeOf(Boom.notImplemented()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.notImplemented(501); + // @ts-expect-error message must be a string + Boom.notImplemented({ foo: 'bar' }); + }); + }); + + describe('badGateway()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.badGateway('this is a bad gateway', 'some data')).toExtend>(); + expectTypeOf(Boom.badGateway('this is a bad gateway', { foo: 'bar' })).toExtend< + Boom.Boom<{ foo: string }> + >(); + expectTypeOf(Boom.badGateway('this is a bad gateway')).toExtend(); + expectTypeOf(Boom.badGateway()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.badGateway(502); + // @ts-expect-error message must be a string + Boom.badGateway({ foo: 'bar' }); + }); + }); + + describe('serverUnavailable()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.serverUnavailable('unavailable', 'some data')).toExtend>(); + expectTypeOf(Boom.serverUnavailable('unavailable', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.serverUnavailable('unavailable')).toExtend(); + expectTypeOf(Boom.serverUnavailable()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.serverUnavailable(503); + // @ts-expect-error message must be a string + Boom.serverUnavailable({ foo: 'bar' }); + }); + }); + + describe('gatewayTimeout()', () => { + it('accepts valid calls', () => { + expectTypeOf(Boom.gatewayTimeout('gateway timeout', 'some data')).toExtend>(); + expectTypeOf(Boom.gatewayTimeout('gateway timeout', { foo: 'bar' })).toExtend>(); + expectTypeOf(Boom.gatewayTimeout('gateway timeout')).toExtend(); + expectTypeOf(Boom.gatewayTimeout()).toExtend(); + }); + + it('rejects invalid calls', () => { + // @ts-expect-error message must be a string + Boom.gatewayTimeout(504); + // @ts-expect-error message must be a string + Boom.gatewayTimeout({ foo: 'bar' }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..849762f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": true, + "isolatedDeclarations": true, + "isolatedModules": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ESNext" + } +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..20fc611 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,24 @@ +import Oxc from '@hapi/oxc-plugin/vitest'; +import { defineConfig } from 'vitest/config'; + +import type { ViteUserConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [Oxc()], + test: { + environment: 'node', + include: ['test/**/*.{js,ts}'], + typecheck: { + enabled: true, + include: ['test/**/*.{js,ts}'], + }, + coverage: { + provider: 'v8', + include: ['src/**'], + exclude: ['**/*.d.ts'], + thresholds: { + 100: true, + }, + }, + }, +}) as ViteUserConfig;