diff --git a/.gitignore b/.gitignore index 87b49c80b..3e031af5b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ lerna-debug.log npm-debug.log packages/*/lib +packages/testing/*/lib .idea coverage/ tsconfig.build.tsbuildinfo diff --git a/integration/rabbitmq/src/rpc/rpc-exception.ts b/integration/rabbitmq/src/rpc/rpc-exception.ts index 61afb9d7d..7f1486e0f 100644 --- a/integration/rabbitmq/src/rpc/rpc-exception.ts +++ b/integration/rabbitmq/src/rpc/rpc-exception.ts @@ -15,10 +15,10 @@ export class RpcException extends Error { isString((this.error as Record).message) ) { this.message = (this.error as Record).message; - } else if (this.constructor) { - this.message = this.constructor.name - .match(/[A-Z][a-z]+|[0-9]+/g) - .join(' '); + } else if (this.constructor.name) { + const matchResult = + this.constructor.name.match(/[A-Z][a-z]+|[0-9]+/g) ?? []; + this.message = matchResult.join(' '); } } diff --git a/lerna.json b/lerna.json index cb30c23da..a35c8dc91 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,8 @@ { - "packages": ["packages/*"], + "packages": [ + "packages/*", + "packages/testing/*" + ], "version": "independent", "npmClient": "yarn", "useWorkspaces": true, diff --git a/package.json b/package.json index efe963f59..a0f5adf65 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "license": "MIT", "workspaces": [ "packages/*", + "packages/testing/*", "integration/*" ], "dependencies": { diff --git a/packages/testing/CHANGELOG.md b/packages/testing/ts-jest/CHANGELOG.md similarity index 100% rename from packages/testing/CHANGELOG.md rename to packages/testing/ts-jest/CHANGELOG.md diff --git a/packages/testing/README.md b/packages/testing/ts-jest/README.md similarity index 98% rename from packages/testing/README.md rename to packages/testing/ts-jest/README.md index 72622cde4..dfd5a2f3c 100644 --- a/packages/testing/README.md +++ b/packages/testing/ts-jest/README.md @@ -117,8 +117,8 @@ For a few more examples on what can be done [the mock.spec](src/mocks.spec.ts) f ## Contribute -Contributions welcome! Read the [contribution guidelines](../../CONTRIBUTING.md) first. +Contributions welcome! Read the [contribution guidelines](../../../CONTRIBUTING.md) first. ## License -[MIT License](../../LICENSE) +[MIT License](../../../LICENSE) diff --git a/packages/testing/package.json b/packages/testing/ts-jest/package.json similarity index 100% rename from packages/testing/package.json rename to packages/testing/ts-jest/package.json diff --git a/packages/testing/src/index.ts b/packages/testing/ts-jest/src/index.ts similarity index 100% rename from packages/testing/src/index.ts rename to packages/testing/ts-jest/src/index.ts diff --git a/packages/testing/src/mocks.spec.ts b/packages/testing/ts-jest/src/mocks.spec.ts similarity index 99% rename from packages/testing/src/mocks.spec.ts rename to packages/testing/ts-jest/src/mocks.spec.ts index 3404a786e..1b0b88b45 100644 --- a/packages/testing/src/mocks.spec.ts +++ b/packages/testing/ts-jest/src/mocks.spec.ts @@ -247,7 +247,7 @@ describe('Mocks', () => { const comparable = createMock(); expect([comparable]).toEqual([comparable]); - }) + }); }); }); diff --git a/packages/testing/src/mocks.ts b/packages/testing/ts-jest/src/mocks.ts similarity index 100% rename from packages/testing/src/mocks.ts rename to packages/testing/ts-jest/src/mocks.ts diff --git a/packages/testing/tsconfig.build.json b/packages/testing/ts-jest/tsconfig.build.json similarity index 100% rename from packages/testing/tsconfig.build.json rename to packages/testing/ts-jest/tsconfig.build.json diff --git a/packages/testing/tsconfig.json b/packages/testing/ts-jest/tsconfig.json similarity index 52% rename from packages/testing/tsconfig.json rename to packages/testing/ts-jest/tsconfig.json index 279b3e923..6f737d390 100644 --- a/packages/testing/tsconfig.json +++ b/packages/testing/ts-jest/tsconfig.json @@ -1,8 +1,10 @@ { - "extends": "../../tsconfig.json", + "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "./lib", "rootDir": "./src" }, - "include": ["./src"] + "include": [ + "./src" + ] } diff --git a/packages/testing/ts-sinon/README.md b/packages/testing/ts-sinon/README.md new file mode 100644 index 000000000..362efbe7f --- /dev/null +++ b/packages/testing/ts-sinon/README.md @@ -0,0 +1,94 @@ +# @golevelup/ts-sinon + +

+version +downloads +license +

+ +## Motivation + +With `@golevelup/ts-sinon`'s `createMock` utility function, you can easily generate deeply nested mock objects for unit +testing, especially useful for mocking complex types like those found in NestJS. + +## Usage + +This package is particularly handy when unit testing components in NestJS, but it's not limited to that. It can +essentially mock any TypeScript interface! + +### Installation + +```sh +npm i @golevelup/ts-sinon --save-dev +``` + +or + +```sh +yarn add @golevelup/ts-sinon --dev +``` + +### Creating Mocks + +1. Import the `createMock` function into your test class. +2. Create a variable and set it equal to the `createMock` function with its generic type input. +3. Use the mock, Luke. + +Here's an example with NestJS' `ExecutionContext`: + +```ts +import { createMock } from '@golevelup/ts-sinon'; +import { ExecutionContext } from '@nestjs/common'; + +describe('Mocked Execution Context', () => { + it('should have a fully mocked Execution Context', () => { + const mockExecutionContext = createMock(); + expect(mockExecutionContext.switchToHttp()).toBeDefined(); + }); +}); +``` + +`createMock` generates all sub-properties as `sinon.stub()`, so you can chain method calls: + +```ts +it('should correctly resolve mocked providers', async () => { + const request = { + key: 'val', + }; + + mockExecutionContext.switchToHttp.returns( + createMock({ + getRequest: () => request, + }) + ); + + const mockResult = mockExecutionContext.switchToHttp().getRequest(); + expect(mockResult).toBe(request); +}); +``` + +You can also easily provide your own mocks: + +```ts +const mockExecutionContext = createMock({ + switchToHttp: () => ({ + getRequest: () => ({ + headers: { + authorization: 'auth', + }, + }), + getResponse: sinon.stub().returns({ data: 'res return data' }), + }), +}); +``` + +> **Note**: When providing your own mocks, the number of times a parent mock function was called includes the times +> needed to set your mocks. + +## Contribute + +Contributions welcome! Read the [contribution guidelines](../../../CONTRIBUTING.md) first. + +## License + +[MIT License](../../../LICENSE) diff --git a/packages/testing/ts-sinon/package-lock.json b/packages/testing/ts-sinon/package-lock.json new file mode 100644 index 000000000..52afc9460 --- /dev/null +++ b/packages/testing/ts-sinon/package-lock.json @@ -0,0 +1,5 @@ +{ + "name": "@golevelup/nestjs-modules", + "version": "0.3.7", + "lockfileVersion": 1 +} diff --git a/packages/testing/ts-sinon/package.json b/packages/testing/ts-sinon/package.json new file mode 100644 index 000000000..87c1c999b --- /dev/null +++ b/packages/testing/ts-sinon/package.json @@ -0,0 +1,61 @@ +{ + "name": "@golevelup/ts-sinon", + "version": "0.0.0", + "description": "", + "author": "Omer Morad ", + "homepage": "https://github.com/golevelup/nestjs#readme", + "license": "MIT", + "keywords": [ + "NestJS", + "testing", + "utilities", + "levelup", + "sinon" + ], + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/golevelup/nestjs.git" + }, + "scripts": { + "build": "tsc --build tsconfig.build.json", + "build:watch": "tsc --build tsconfig.build.json --watch", + "test": "jest" + }, + "bugs": { + "url": "https://github.com/golevelup/nestjs/issues" + }, + "peerDependencies": { + "sinon": "^14.x" + }, + "devDependencies": { + "sinon": "^14.x", + "@types/sinon": "^10.0.15" + }, + "publishConfig": { + "access": "public" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".spec.ts$", + "transform": { + "^.+\\.ts$": "ts-jest" + }, + "coverageDirectory": "../coverage", + "testEnvironment": "node" + }, + "gitHead": "6f97aab8ce9d65dc074750a3ee467ec5ff3b9908" +} diff --git a/packages/testing/ts-sinon/src/index.ts b/packages/testing/ts-sinon/src/index.ts new file mode 100644 index 000000000..aabd0765c --- /dev/null +++ b/packages/testing/ts-sinon/src/index.ts @@ -0,0 +1 @@ +export * from './mocks'; diff --git a/packages/testing/ts-sinon/src/mocks.spec.ts b/packages/testing/ts-sinon/src/mocks.spec.ts new file mode 100644 index 000000000..09f685743 --- /dev/null +++ b/packages/testing/ts-sinon/src/mocks.spec.ts @@ -0,0 +1,307 @@ +import { ExecutionContext } from '@nestjs/common'; +import { HttpArgumentsHost } from '@nestjs/common/interfaces'; +import { createMock, DeepMocked } from './mocks'; +import { SinonStub } from 'sinon'; +import { Test, TestingModule } from '@nestjs/testing'; + +interface TestInterface { + someNum: number; + someBool: boolean; + optional: string | undefined; + func: (num: number, str: string) => boolean; +} + +class TestClass { + someProperty!: number; + + someMethod() { + return 42; + } +} + +describe('Mocks', () => { + const request = { + headers: { + authorization: 'auth', + }, + }; + + describe('user provided', () => { + it('should convert user provided test object to mocks', () => { + const request = { + headers: { + authorization: 'auth', + }, + }; + const mock = createMock({ + switchToHttp: () => ({ + getRequest: () => request, + }), + }); + + const result = mock.switchToHttp().getRequest(); + + expect(result).toBe(request); + expect(mock.switchToHttp.calledOnce).toBeTruthy(); + }); + + it('should work with truthy values properties', () => { + const mock = createMock({ + someNum: 1, + someBool: true, + }); + + expect(mock.someNum).toBe(1); + expect(mock.someBool).toBe(true); + }); + + it('should work with falsy values properties', () => { + const mock = createMock({ + someNum: 0, + someBool: false, + }); + + expect(mock.someNum).toBe(0); + expect(mock.someBool).toBe(false); + }); + + it('should work with optional values explicitly returning undefined', () => { + const mock = createMock({ + optional: undefined, + }); + + expect(mock.optional).toBe(undefined); + }); + + it('should work with properties and functions', () => { + const mock = createMock({ + someNum: 42, + func: () => false, + }); + + const num = mock.someNum; + expect(num).toBe(42); + + const funcResult = mock.func(42, '42'); + expect(funcResult).toBe(false); + expect(mock.func.calledOnce).toBeTruthy(); + expect(mock.func.calledWith(42, '42')).toBeTruthy(); + }); + + it('should work with classes', () => { + const mock = createMock(undefined, { name: 'TestClass' }); + + mock.someMethod.returns(42); + + const result = mock.someMethod(); + expect(result).toBe(42); + }); + + it('should work with partial objects and potentially undefined methods', () => { + type TypeWithOptionalProps = { + maybe?: () => number; + another: () => boolean; + }; + + const mock = createMock(); + (mock.maybe as SinonStub)?.callsFake(() => 42); + + const result = mock.maybe?.(); + + expect(result).toBe(42); + }); + + it('should work with promises', async () => { + type TypeWithPromiseReturningFunctions = { + doSomethingAsync: () => Promise; + }; + + const mock = createMock({ + doSomethingAsync: async () => 42, + }); + + const result = await mock.doSomethingAsync(); + expect(result).toBe(42); + expect(mock.doSomethingAsync.calledOnce).toBeTruthy(); + }); + + it('should work with unknown properties', () => { + class Base { + field?: unknown; + } + + class Test { + get base(): Base { + return undefined as any; + } + } + + const base = createMock(); + const test = createMock({ + base, + }); + + expect(test.base).toEqual(base); + }); + }); + + describe('auto mocked', () => { + it('should auto mock functions that are not provided by the user', () => { + const mock = createMock({ + switchToHttp: () => ({ + getRequest: () => request, + }), + }); + + const first = mock.switchToRpc(); + const second = mock.switchToRpc(); + const third = mock.switchToWs(); + + expect(mock.switchToRpc.calledTwice).toBeTruthy(); + expect(mock.switchToWs.calledOnce).toBeTruthy(); + + expect(first.getContext).toBeDefined(); + expect(second.getContext).toBeDefined(); + expect(third.getClient).toBeDefined(); + }); + + it('should allow for mock implementation on auto mocked properties', () => { + const executionContextMock = createMock(); + const httpArgsHost = createMock({ + getRequest: () => request, + }); + + executionContextMock.switchToHttp.callsFake(() => httpArgsHost); + + const result = executionContextMock.switchToHttp().getRequest(); + expect(result).toBe(request); + expect(httpArgsHost.getRequest.calledOnce).toBeTruthy(); + }); + + it('should automock promises so that they are awaitable', async () => { + type TypeWithPromiseReturningFunctions = { + doSomethingAsync: () => Promise; + }; + + const mock = createMock(); + + const result = await mock.doSomethingAsync(); + expect(result).toBeDefined(); + expect(mock.doSomethingAsync.calledOnce).toBeTruthy(); + }); + + it('should automock objects returned from automocks', () => { + const mock = createMock(); + + mock.switchToHttp().getRequest.callsFake(() => request); + + const request1 = mock.switchToHttp().getRequest(); + const request2 = mock.switchToHttp().getRequest(); + expect(request1).toBe(request); + expect(request2).toBe(request); + + expect(mock.switchToHttp.calledThrice).toBeTruthy(); + expect( + (mock.switchToHttp().getRequest as SinonStub).calledTwice + ).toBeTruthy(); + }); + + it('should automock objects returned from automocks recursively', () => { + interface One { + getNumber: () => number; + } + + interface Two { + getOne: () => One; + } + + interface Three { + getTwo: () => Two; + } + + const mock = createMock(); + + mock.getTwo().getOne().getNumber.returns(42); + + const result = mock.getTwo().getOne().getNumber(); + + expect(result).toBe(42); + }); + + describe('constructor', () => { + it('should have constructor defined', () => { + class Service {} + + const mock = createMock(); + + expect(mock.constructor).toBeDefined(); + }); + + it('should have the same constructor defined', () => { + class Service {} + + const mock = createMock(); + + expect(mock.constructor).toEqual(mock.constructor); + }); + + it(`should allow mocks to be equal`, () => { + class Service {} + + const comparable = createMock(); + + expect([comparable]).toEqual([comparable]); + }); + }); + }); + + describe('Nest DI', () => { + let module: TestingModule; + let mockedProvider: DeepMocked; + let dependentProvider: { dependent: () => string }; + const diToken = Symbol('diToken'); + const dependentToken = Symbol('dependentToken'); + + beforeEach(async () => { + module = await Test.createTestingModule({ + providers: [ + { + provide: diToken, + useValue: createMock({ + getType: () => 'something', + }), + }, + { + inject: [diToken], + provide: dependentToken, + useFactory: (dep: DeepMocked) => ({ + dependent: dep.getType, + }), + }, + ], + }).compile(); + + mockedProvider = module.get>(diToken); + dependentProvider = + module.get<{ dependent: () => string }>(dependentToken); + }); + + it('should correctly resolve mocked providers', async () => { + const request = { + key: 'val', + }; + + mockedProvider.switchToHttp.returns( + createMock({ + getRequest: () => request, + }) + ); + + const mockResult = mockedProvider.switchToHttp().getRequest(); + expect(mockResult).toBe(request); + + const dependentResult = dependentProvider.dependent(); + expect(dependentResult).toBe('something'); + }); + }); +}); diff --git a/packages/testing/ts-sinon/src/mocks.ts b/packages/testing/ts-sinon/src/mocks.ts new file mode 100644 index 000000000..e2b76af09 --- /dev/null +++ b/packages/testing/ts-sinon/src/mocks.ts @@ -0,0 +1,116 @@ +import * as sinon from 'sinon'; +import { SinonStub } from 'sinon'; + +type DeepPartial = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : unknown extends T[P] + ? T[P] + : DeepPartial; +}; + +export type PartialFuncReturn = { + [K in keyof T]?: T[K] extends (...args: infer A) => infer U + ? (...args: A) => PartialFuncReturn + : DeepPartial; +}; + +export type DeepMocked = { + [Key in keyof T]: T[Key] extends (...args: infer A) => infer U + ? SinonStub & ((...args: A) => DeepMocked) + : T[Key]; +} & T; + +export type MockCreationOptions = { + name?: string; +}; + +const createDeepMockProxy = (propName: string): SinonStub => { + const proxyObject = new Proxy({}, createDeepMockHandler(propName)); + return sinon.stub().callsFake(() => proxyObject); +}; + +const createDeepMockHandler = (propName: string) => { + const propertyCache = new Map< + PropertyKey, + SinonStub | DeepPartial + >(); + + return { + get: (targetObject: DeepPartial, prop: PropertyKey) => { + if (propertyCache.has(prop)) { + return propertyCache.get(prop); + } + + const targetProperty = targetObject[prop]; + let mockedProperty; + + if (prop in targetObject) { + mockedProperty = + typeof targetProperty === 'function' ? sinon.stub() : targetProperty; + } else if (prop.toString() === 'then') { + mockedProperty = undefined; + } else { + mockedProperty = createDeepMockProxy(propName); + } + + propertyCache.set(prop, mockedProperty); + + return mockedProperty; + }, + }; +}; + +const createMockHandler = (name: string) => { + const cache = new Map>(); + + return { + get: (targetObject: DeepPartial, prop: PropertyKey) => { + if ( + prop === 'inspect' || + prop === 'then' || + (typeof prop === 'symbol' && + prop.toString() === 'Symbol(util.inspect.custom)') + ) { + return undefined; + } + + if (cache.has(prop)) { + return cache.get(prop); + } + + const targetProperty = targetObject[prop]; + let mockedProp; + + if (prop in targetObject) { + if (typeof targetProperty === 'function') { + mockedProp = sinon.stub().callsFake(() => { + const result = targetProperty(); + return typeof result === 'function' ? sinon.stub(result) : result; + }); + } else { + mockedProp = targetProperty; + } + } else if (prop === 'constructor') { + mockedProp = () => undefined; + } else { + mockedProp = createDeepMockProxy(`${name}.${prop.toString()}`); + } + + cache.set(prop, mockedProp); + return mockedProp; + }, + }; +}; + +export const createMock = ( + partialObject: PartialFuncReturn = {}, + options: MockCreationOptions = {} +): DeepMocked => { + const { name = 'mock' } = options; + const proxyObject = new Proxy(partialObject, createMockHandler(name)); + + return proxyObject as DeepMocked; +}; diff --git a/packages/testing/ts-sinon/tsconfig.build.json b/packages/testing/ts-sinon/tsconfig.build.json new file mode 100644 index 000000000..b590f917e --- /dev/null +++ b/packages/testing/ts-sinon/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "./src/**/*.spec.ts", + "node_modules" + ] +} diff --git a/packages/testing/ts-sinon/tsconfig.json b/packages/testing/ts-sinon/tsconfig.json new file mode 100644 index 000000000..6f737d390 --- /dev/null +++ b/packages/testing/ts-sinon/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src" + }, + "include": [ + "./src" + ] +} diff --git a/yarn.lock b/yarn.lock index 735f10be8..fdc37ead6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -633,21 +633,6 @@ unique-filename "^1.1.1" which "^1.3.1" -"@golevelup/nestjs-common@^1.4.5": - version "1.4.5" - resolved "https://registry.yarnpkg.com/@golevelup/nestjs-common/-/nestjs-common-1.4.5.tgz#28557ae2d7cfda60d4f845e1f2ff1da90b337d4d" - integrity sha512-WqxGAP4KZjvUea/lYCEfFXB8fS3NwxEWgrQpz2H8jusbz11OSrLECv5TNU9RIRKFnUPbGevy45dvKHF2JTQfJw== - dependencies: - lodash "^4.17.21" - nanoid "^3.2.0" - -"@golevelup/nestjs-discovery@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@golevelup/nestjs-discovery/-/nestjs-discovery-3.0.1.tgz#7ad7e2c5ac245bd315ff4a98d4870248dace535a" - integrity sha512-kK/GBYVxb8XGlwXgtCWAkPOwDVh7dXyLRaoZuk2bBYntV3DZkYGAIbLKOFTGz+MGz71vEeQ9bGLP7cHKtCee4g== - dependencies: - lodash "^4.17.15" - "@hasura/metadata@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@hasura/metadata/-/metadata-1.0.2.tgz#0e212a349a176108c1f2572faa03317d21c2f052" @@ -1834,6 +1819,27 @@ dependencies: type-detect "4.0.8" +"@sinonjs/commons@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" + integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== + dependencies: + type-detect "4.0.8" + +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers@^8.0.1": version "8.1.0" resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz#3fdc2b6cb58935b21bfb8d1625eb1300484316e7" @@ -1841,6 +1847,27 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@sinonjs/fake-timers@^9.1.2": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" + integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@sinonjs/samsam@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-7.0.1.tgz#5b5fa31c554636f78308439d220986b9523fc51f" + integrity sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw== + dependencies: + "@sinonjs/commons" "^2.0.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.1": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" + integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -2123,6 +2150,18 @@ "@types/mime" "*" "@types/node" "*" +"@types/sinon@^10.0.15": + version "10.0.15" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.15.tgz#513fded9c3cf85e589bbfefbf02b2a0541186b48" + integrity sha512-3lrFNQG0Kr2LDzvjyjB6AMJk4ge+8iYhQfdnSwIwlG88FUOV43kPcQqDZkDa/h3WSZy6i8Fr0BSjfQtB1B3xuQ== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "8.1.2" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e" + integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA== + "@types/stack-utils@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" @@ -3949,6 +3988,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + dir-glob@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" @@ -6737,6 +6781,11 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" +just-extend@^4.0.2: + version "4.2.1" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" + integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -7500,7 +7549,7 @@ mz@^2.5.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.2.0, nanoid@^3.3.6: +nanoid@^3.3.6: version "3.3.6" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== @@ -7547,6 +7596,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^5.1.2: + version "5.1.4" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.4.tgz#491ce7e7307d4ec546f5a659b2efe94a18b4bbc0" + integrity sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg== + dependencies: + "@sinonjs/commons" "^2.0.0" + "@sinonjs/fake-timers" "^10.0.2" + "@sinonjs/text-encoding" "^0.7.1" + just-extend "^4.0.2" + path-to-regexp "^1.7.0" + node-fetch-npm@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.4.tgz#6507d0e17a9ec0be3bec516958a497cec54bf5a4" @@ -8153,6 +8213,13 @@ path-to-regexp@3.2.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -9095,6 +9162,18 @@ simple-update-notifier@^1.0.7: dependencies: semver "~7.0.0" +sinon@^14.x: + version "14.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.2.tgz#585a81a3c7b22cf950762ac4e7c28eb8b151c46f" + integrity sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w== + dependencies: + "@sinonjs/commons" "^2.0.0" + "@sinonjs/fake-timers" "^9.1.2" + "@sinonjs/samsam" "^7.0.1" + diff "^5.0.0" + nise "^5.1.2" + supports-color "^7.2.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -9591,7 +9670,7 @@ supports-color@^5.3.0, supports-color@^5.5.0: dependencies: has-flag "^3.0.0" -supports-color@^7.0.0, supports-color@^7.1.0: +supports-color@^7.0.0, supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -9983,7 +10062,7 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8: +type-detect@4.0.8, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==