From 526e7f17e5c08971aeebaacc48d467b257c2bfd2 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 10 May 2019 18:52:11 -0700 Subject: [PATCH 1/5] tiny bits of clean up to test code --- src/App.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.spec.ts b/src/App.spec.ts index c62c76931..3d1e7c55a 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -1,4 +1,4 @@ -// tslint:disable:ter-prefer-arrow-callback typedef no-implicit-dependencies no-this-assignment +// tslint:disable:no-implicit-dependencies import 'mocha'; import { EventEmitter } from 'events'; import sinon, { SinonSpy } from 'sinon'; @@ -719,7 +719,7 @@ function createDummyReceiverEvent(): ReceiverEvent { const noop = () => { }; // tslint:disable-line:no-empty const noopMiddleware = ({ next }: { next: NextMiddleware; }) => { next(); }; const noopAuthorize = (() => Promise.resolve({})); -function delay(ms: number = 0) { +function delay(ms: number = 0): Promise { return new Promise((resolve) => { setTimeout(resolve, ms); }); From 18181a49dbe66ecdd1326db8fc4a29d339cfa947 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 10 May 2019 18:53:28 -0700 Subject: [PATCH 2/5] fixes #185, adds conversation context middleware tests --- src/conversation-store.spec.ts | 219 +++++++++++++++++++++++++++++++++ src/conversation-store.ts | 2 + 2 files changed, 221 insertions(+) create mode 100644 src/conversation-store.spec.ts diff --git a/src/conversation-store.spec.ts b/src/conversation-store.spec.ts new file mode 100644 index 000000000..ad1e8e144 --- /dev/null +++ b/src/conversation-store.spec.ts @@ -0,0 +1,219 @@ +// tslint:disable:no-implicit-dependencies no-object-literal-type-assertion +import 'mocha'; +import { assert } from 'chai'; +import sinon, { SinonSpy } from 'sinon'; +import rewiremock from 'rewiremock'; +import { ConversationStore } from './conversation-store'; +import { AnyMiddlewareArgs, NextMiddleware, Context } from './types'; +import { Logger } from '@slack/logger'; + +interface DummyContext { + conversation?: ConversationState; + updateConversation?: (c: ConversationState) => Promise; +} + +describe('conversationContext middleware', () => { + it('should forward events that have no conversation ID', async () => { + // Arrange + // conversationId property is omitted from return value + const fakeGetTypeAndConversation = sinon.fake.returns({}); + const fakeStore = createFakeStore(); + const fakeLogger = createFakeLogger(); + const fakeNext = sinon.fake(); + const dummyContext: DummyContext = {}; + const { conversationContext } = await importConversationStore( + withGetTypeAndConversation(fakeGetTypeAndConversation), + ); + const fakeArgs = { body: {}, context: dummyContext, next: fakeNext } as unknown as MiddlewareArgs; + + // Act + const middleware = conversationContext(fakeStore, fakeLogger); + middleware(fakeArgs); + + // Assert + assert(fakeLogger.debug.called); + assert(fakeNext.called); + assert.notProperty(dummyContext, 'updateConversation'); + assert.notProperty(dummyContext, 'conversation'); + }); + + // TODO: test that expiresAt is passed through on calls to store.set + + it('should add to the context for events within a conversation that was not previously stored', async () => { + // Arrange + const dummyConversationState = Symbol(); + const dummyConversationId = 'CONVERSATION_ID'; + const dummyStoreSetResult = Symbol(); + const fakeGetTypeAndConversation = sinon.fake.returns({ conversationId: dummyConversationId }); + const fakeStore = createFakeStore( + sinon.fake.rejects(new Error('Test conversation missing')), + sinon.fake.resolves(dummyStoreSetResult), + ); + const fakeLogger = createFakeLogger(); + const { fn: next, promise: onNextFirstCall } = wrapToResolveOnFirstCall(assertions); + const dummyContext: DummyContext = {}; + const { conversationContext } = await importConversationStore( + withGetTypeAndConversation(fakeGetTypeAndConversation), + ); + const fakeArgs = { next, body: {}, context: dummyContext } as unknown as MiddlewareArgs; + + // Act + const middleware = conversationContext(fakeStore, fakeLogger); + middleware(fakeArgs); + + // Assert + async function assertions(...args: any[]): Promise { + assert.notExists(args[0]); + assert.notProperty(dummyContext, 'conversation'); + if (dummyContext.updateConversation !== undefined) { + const result = await dummyContext.updateConversation(dummyConversationState); + assert.equal(result, dummyStoreSetResult); + assert(fakeStore.set.calledOnce); + assert(fakeStore.set.calledWith(dummyConversationId, dummyConversationState)); + } else { + assert.fail(); + } + } + return onNextFirstCall; + }); + + it('should add to the context for events within a conversation that was previously stored', async () => { + // Arrange + const dummyConversationState = Symbol(); + const dummyConversationId = 'CONVERSATION_ID'; + const dummyStoreSetResult = Symbol(); + const fakeGetTypeAndConversation = sinon.fake.returns({ conversationId: dummyConversationId }); + const fakeStore = createFakeStore( + sinon.fake.resolves(dummyConversationState), + sinon.fake.resolves(dummyStoreSetResult), + ); + const fakeLogger = createFakeLogger(); + const { fn: next, promise: onNextFirstCall } = wrapToResolveOnFirstCall(assertions); + const dummyContext: DummyContext = {}; + const { conversationContext } = await importConversationStore( + withGetTypeAndConversation(fakeGetTypeAndConversation), + ); + const fakeArgs = { next, body: {}, context: dummyContext } as unknown as MiddlewareArgs; + + // Act + const middleware = conversationContext(fakeStore, fakeLogger); + middleware(fakeArgs); + + // Assert + async function assertions(...args: any[]): Promise { + assert.notExists(args[0]); + assert.equal(dummyContext.conversation, dummyConversationState); + if (dummyContext.updateConversation !== undefined) { + const newDummyConversationState = Symbol(); + const result = await dummyContext.updateConversation(newDummyConversationState); + assert.equal(result, dummyStoreSetResult); + assert(fakeStore.set.calledOnce); + assert(fakeStore.set.calledWith(dummyConversationId, newDummyConversationState)); + } else { + assert.fail(); + } + } + return onNextFirstCall; + }); +}); + +/* Testing Harness */ + +type MiddlewareArgs = AnyMiddlewareArgs & { next: NextMiddleware, context: Context }; + +// Loading the system under test using overrides +async function importConversationStore( + overrides: Override = {}, +): Promise { + return rewiremock.module(() => import('./conversation-store'), overrides); +} + +// Composable overrides +// TODO: DRY this up with the duplicate definition in App.spec.ts +interface Override { + [packageName: string]: { + [exportName: string]: any; + }; +} + +function withGetTypeAndConversation(spy: SinonSpy): Override { + return { + './helpers': { + getTypeAndConversation: spy, + }, + }; +} + +// TODO: DRY up fake logger code + +interface FakeLogger extends Logger { + setLevel: SinonSpy, ReturnType>; + setName: SinonSpy, ReturnType>; + debug: SinonSpy, ReturnType>; + info: SinonSpy, ReturnType>; + warn: SinonSpy, ReturnType>; + error: SinonSpy, ReturnType>; +} + +function createFakeLogger(): FakeLogger { + return { + // NOTE: the two casts are because of a TypeScript inconsistency with tuple types and any[]. all tuple types + // should be assignable to any[], but TypeScript doesn't think so. + setLevel: sinon.fake() as SinonSpy, ReturnType>, + setName: sinon.fake() as SinonSpy, ReturnType>, + debug: sinon.fake(), + info: sinon.fake(), + warn: sinon.fake(), + error: sinon.fake(), + }; +} + +interface FakeStore extends ConversationStore { + set: SinonSpy, ReturnType>; + get: SinonSpy, ReturnType>; +} + +function createFakeStore( + getSpy: SinonSpy = sinon.fake.resolves(undefined), + setSpy: SinonSpy = sinon.fake.resolves({}), +): FakeStore { + return { + set: setSpy as SinonSpy, ReturnType>, + get: getSpy as SinonSpy, ReturnType>, + }; +} + +function wrapToResolveOnFirstCall void>( + original: T, + timeoutMs: number = 1000, +): { fn: (...args: Parameters) => Promise; promise: Promise; } { + // tslint:disable-next-line:no-empty + let firstCallResolve: (value?: void | PromiseLike) => void = () => { }; + let firstCallReject: (reason?: any) => void = () => { }; // tslint:disable-line:no-empty + + const firstCallPromise: Promise = new Promise((resolve, reject) => { + firstCallResolve = resolve; + firstCallReject = reject; + }); + + const wrapped = async function (this: ThisParameterType, ...args: Parameters): Promise { + try { + await original.call(this, ...args); + firstCallResolve(); + } catch (error) { + firstCallReject(error); + } + }; + + setTimeout( + () => { + firstCallReject(new Error('First call to function took longer than expected')); + }, + timeoutMs, + ); + + return { + promise: firstCallPromise, + fn: wrapped, + }; +} diff --git a/src/conversation-store.ts b/src/conversation-store.ts index 64a9bae6e..13220d53f 100644 --- a/src/conversation-store.ts +++ b/src/conversation-store.ts @@ -59,6 +59,7 @@ export function conversationContext( const { body, context, next } = args; const { conversationId } = getTypeAndConversation(body as any); if (conversationId !== undefined) { + // TODO: expiresAt is not passed through to store.set context.updateConversation = (conversation: ConversationState) => store.set(conversationId, conversation); store.get(conversationId) .then((conversationState) => { @@ -71,6 +72,7 @@ export function conversationContext( .then(next); } else { logger.debug('No conversation ID for incoming event'); + next(); } }; } From 0c75c3dddd11a1b6b36ccab03d90ff444c2d067e Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 10 May 2019 19:19:12 -0700 Subject: [PATCH 3/5] refactor test helpers into single file, adjust coverage config to ignore helpers --- .nycrc.json | 3 +- src/App.spec.ts | 55 +--------------------------------- src/conversation-store.spec.ts | 47 ++++++----------------------- src/helpers.ts | 2 ++ src/test-helpers.ts | 55 ++++++++++++++++++++++++++++++++++ tsconfig.json | 3 +- 6 files changed, 71 insertions(+), 94 deletions(-) create mode 100644 src/test-helpers.ts diff --git a/.nycrc.json b/.nycrc.json index e2bcba9cd..bf6690f0a 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -3,7 +3,8 @@ "src/**/*.ts" ], "exclude": [ - "**/*.spec.ts" + "**/*.spec.ts", + "src/test-helpers.ts" ], "reporter": ["lcov"], "extension": [ diff --git a/src/App.spec.ts b/src/App.spec.ts index 3d1e7c55a..8dee5ed3c 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -3,11 +3,11 @@ import 'mocha'; import { EventEmitter } from 'events'; import sinon, { SinonSpy } from 'sinon'; import { assert } from 'chai'; +import { Override, mergeOverrides, createFakeLogger } from './test-helpers'; import rewiremock from 'rewiremock'; import { ErrorCode } from './errors'; import { Receiver, ReceiverEvent, SayFn, NextMiddleware } from './types'; import { ConversationStore } from './conversation-store'; -import { Logger } from '@slack/logger'; describe('App', () => { describe('constructor', () => { @@ -567,12 +567,6 @@ async function importApp( } // Composable overrides -interface Override { - [packageName: string]: { - [exportName: string]: any; - }; -} - function withNoopWebClient(): Override { return { '@slack/web-api': { @@ -589,7 +583,6 @@ function withNoopAppMetadata(): Override { }; } -// TODO: see if we can use a partial type for the return value function withSuccessfulBotUserFetchingWebClient(botId: string, botUserId: string): Override { return { '@slack/web-api': { @@ -639,30 +632,6 @@ function withConversationContext(spy: SinonSpy): Override { }; } -function mergeOverrides(...overrides: Override[]): Override { - let currentOverrides: Override = {}; - for (const override of overrides) { - currentOverrides = mergeObjProperties(currentOverrides, override); - } - return currentOverrides; -} - -function mergeObjProperties(first: Override, second: Override): Override { - const merged: Override = {}; - const props = Object.keys(first).concat(Object.keys(second)); - for (const prop of props) { - if (second[prop] === undefined && first[prop] !== undefined) { - merged[prop] = first[prop]; - } else if (first[prop] === undefined && second[prop] !== undefined) { - merged[prop] = second[prop]; - } else { - // second always overwrites the first - merged[prop] = { ...first[prop], ...second[prop] }; - } - } - return merged; -} - // Fakes type FakeReceiver = SinonSpy & EventEmitter & { start: SinonSpy, ReturnType>; @@ -679,28 +648,6 @@ function createFakeReceiver( return mock as FakeReceiver; } -interface FakeLogger extends Logger { - setLevel: SinonSpy, ReturnType>; - setName: SinonSpy, ReturnType>; - debug: SinonSpy, ReturnType>; - info: SinonSpy, ReturnType>; - warn: SinonSpy, ReturnType>; - error: SinonSpy, ReturnType>; -} - -function createFakeLogger(): FakeLogger { - return { - // NOTE: the two casts are because of a TypeScript inconsistency with tuple types and any[]. all tuple types - // should be assignable to any[], but TypeScript doesn't think so. - setLevel: sinon.fake() as SinonSpy, ReturnType>, - setName: sinon.fake() as SinonSpy, ReturnType>, - debug: sinon.fake(), - info: sinon.fake(), - warn: sinon.fake(), - error: sinon.fake(), - }; -} - // Dummies (values that have no real behavior but pass through the system opaquely) function createDummyReceiverEvent(): ReceiverEvent { // NOTE: this is a degenerate ReceiverEvent that would successfully pass through the App. it happens to look like a diff --git a/src/conversation-store.spec.ts b/src/conversation-store.spec.ts index ad1e8e144..baeb840c8 100644 --- a/src/conversation-store.spec.ts +++ b/src/conversation-store.spec.ts @@ -1,16 +1,11 @@ -// tslint:disable:no-implicit-dependencies no-object-literal-type-assertion +// tslint:disable:no-implicit-dependencies import 'mocha'; import { assert } from 'chai'; import sinon, { SinonSpy } from 'sinon'; +import { Override, createFakeLogger } from './test-helpers'; import rewiremock from 'rewiremock'; import { ConversationStore } from './conversation-store'; import { AnyMiddlewareArgs, NextMiddleware, Context } from './types'; -import { Logger } from '@slack/logger'; - -interface DummyContext { - conversation?: ConversationState; - updateConversation?: (c: ConversationState) => Promise; -} describe('conversationContext middleware', () => { it('should forward events that have no conversation ID', async () => { @@ -121,6 +116,11 @@ describe('conversationContext middleware', () => { type MiddlewareArgs = AnyMiddlewareArgs & { next: NextMiddleware, context: Context }; +interface DummyContext { + conversation?: ConversationState; + updateConversation?: (c: ConversationState) => Promise; +} + // Loading the system under test using overrides async function importConversationStore( overrides: Override = {}, @@ -129,13 +129,6 @@ async function importConversationStore( } // Composable overrides -// TODO: DRY this up with the duplicate definition in App.spec.ts -interface Override { - [packageName: string]: { - [exportName: string]: any; - }; -} - function withGetTypeAndConversation(spy: SinonSpy): Override { return { './helpers': { @@ -144,30 +137,7 @@ function withGetTypeAndConversation(spy: SinonSpy): Override { }; } -// TODO: DRY up fake logger code - -interface FakeLogger extends Logger { - setLevel: SinonSpy, ReturnType>; - setName: SinonSpy, ReturnType>; - debug: SinonSpy, ReturnType>; - info: SinonSpy, ReturnType>; - warn: SinonSpy, ReturnType>; - error: SinonSpy, ReturnType>; -} - -function createFakeLogger(): FakeLogger { - return { - // NOTE: the two casts are because of a TypeScript inconsistency with tuple types and any[]. all tuple types - // should be assignable to any[], but TypeScript doesn't think so. - setLevel: sinon.fake() as SinonSpy, ReturnType>, - setName: sinon.fake() as SinonSpy, ReturnType>, - debug: sinon.fake(), - info: sinon.fake(), - warn: sinon.fake(), - error: sinon.fake(), - }; -} - +// Fakes interface FakeStore extends ConversationStore { set: SinonSpy, ReturnType>; get: SinonSpy, ReturnType>; @@ -183,6 +153,7 @@ function createFakeStore( }; } +// Utility functions function wrapToResolveOnFirstCall void>( original: T, timeoutMs: number = 1000, diff --git a/src/helpers.ts b/src/helpers.ts index f59e70db5..e1515b565 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -55,6 +55,8 @@ export function getTypeAndConversation(body: any): { type?: IncomingEventType, c return {}; } +/* istanbul ignore next */ + /** Helper that should never be called, but is useful for exhaustiveness checking in conditional branches */ export function assertNever(x: never): never { throw new Error(`Unexpected object: ${x}`); diff --git a/src/test-helpers.ts b/src/test-helpers.ts new file mode 100644 index 000000000..e97bf8bc2 --- /dev/null +++ b/src/test-helpers.ts @@ -0,0 +1,55 @@ +// tslint:disable:no-implicit-dependencies +import sinon, { SinonSpy } from 'sinon'; +import { Logger } from '@slack/logger'; + +export interface Override { + [packageName: string]: { + [exportName: string]: any; + }; +} + +export function mergeOverrides(...overrides: Override[]): Override { + let currentOverrides: Override = {}; + for (const override of overrides) { + currentOverrides = mergeObjProperties(currentOverrides, override); + } + return currentOverrides; +} + +function mergeObjProperties(first: Override, second: Override): Override { + const merged: Override = {}; + const props = Object.keys(first).concat(Object.keys(second)); + for (const prop of props) { + if (second[prop] === undefined && first[prop] !== undefined) { + merged[prop] = first[prop]; + } else if (first[prop] === undefined && second[prop] !== undefined) { + merged[prop] = second[prop]; + } else { + // second always overwrites the first + merged[prop] = { ...first[prop], ...second[prop] }; + } + } + return merged; +} + +export interface FakeLogger extends Logger { + setLevel: SinonSpy, ReturnType>; + setName: SinonSpy, ReturnType>; + debug: SinonSpy, ReturnType>; + info: SinonSpy, ReturnType>; + warn: SinonSpy, ReturnType>; + error: SinonSpy, ReturnType>; +} + +export function createFakeLogger(): FakeLogger { + return { + // NOTE: the two casts are because of a TypeScript inconsistency with tuple types and any[]. all tuple types + // should be assignable to any[], but TypeScript doesn't think so. + setLevel: sinon.fake() as SinonSpy, ReturnType>, + setName: sinon.fake() as SinonSpy, ReturnType>, + debug: sinon.fake(), + info: sinon.fake(), + warn: sinon.fake(), + error: sinon.fake(), + }; +} diff --git a/tsconfig.json b/tsconfig.json index 5b251ca25..cdcd897d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -63,6 +63,7 @@ "src/**/*" ], "exclude": [ - "src/**/*.spec.ts" + "**/*.spec.ts", + "src/test-helpers.ts" ] } From 77ebe70dc55310ea98a31738d90b8c1bc2abd047 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Fri, 10 May 2019 19:19:46 -0700 Subject: [PATCH 4/5] remove superfluous comment --- src/ExpressReceiver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ExpressReceiver.ts b/src/ExpressReceiver.ts index 8236bcf79..1667caaf3 100644 --- a/src/ExpressReceiver.ts +++ b/src/ExpressReceiver.ts @@ -135,8 +135,6 @@ export default class ExpressReceiver extends EventEmitter implements Receiver { } } -// TODO: respond to url_verification, and also help a beginner set up Events API (maybe adopt the CLI verify tool) - const respondToSslCheck: RequestHandler = (req, res, next) => { if (req.body && req.body.ssl_check) { res.send(); From b0642caf8deb2cb1fe43cd2ed899e7ef0541f387 Mon Sep 17 00:00:00 2001 From: Ankur Oberoi Date: Mon, 13 May 2019 11:02:52 -0700 Subject: [PATCH 5/5] adds tests for MemoryStore --- src/App.spec.ts | 7 +--- src/conversation-store.spec.ts | 76 +++++++++++++++++++++++++++++++++- src/test-helpers.ts | 6 +++ 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/App.spec.ts b/src/App.spec.ts index 8dee5ed3c..040334524 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -3,7 +3,7 @@ import 'mocha'; import { EventEmitter } from 'events'; import sinon, { SinonSpy } from 'sinon'; import { assert } from 'chai'; -import { Override, mergeOverrides, createFakeLogger } from './test-helpers'; +import { Override, mergeOverrides, createFakeLogger, delay } from './test-helpers'; import rewiremock from 'rewiremock'; import { ErrorCode } from './errors'; import { Receiver, ReceiverEvent, SayFn, NextMiddleware } from './types'; @@ -666,10 +666,5 @@ function createDummyReceiverEvent(): ReceiverEvent { const noop = () => { }; // tslint:disable-line:no-empty const noopMiddleware = ({ next }: { next: NextMiddleware; }) => { next(); }; const noopAuthorize = (() => Promise.resolve({})); -function delay(ms: number = 0): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} // TODO: swap out rewiremock for proxyquire to see if it saves execution time diff --git a/src/conversation-store.spec.ts b/src/conversation-store.spec.ts index baeb840c8..c3bac9494 100644 --- a/src/conversation-store.spec.ts +++ b/src/conversation-store.spec.ts @@ -1,8 +1,8 @@ // tslint:disable:no-implicit-dependencies import 'mocha'; -import { assert } from 'chai'; +import { assert, AssertionError } from 'chai'; import sinon, { SinonSpy } from 'sinon'; -import { Override, createFakeLogger } from './test-helpers'; +import { Override, createFakeLogger, delay } from './test-helpers'; import rewiremock from 'rewiremock'; import { ConversationStore } from './conversation-store'; import { AnyMiddlewareArgs, NextMiddleware, Context } from './types'; @@ -112,6 +112,78 @@ describe('conversationContext middleware', () => { }); }); +describe('MemoryStore', () => { + describe('constructor', () => { + it('should initialize successfully', async () => { + // Arrange + const { MemoryStore } = await importConversationStore(); + + // Act + const store = new MemoryStore(); + + // Assert + assert.isOk(store); + }); + }); + + // NOTE: there's no good way to fetch the contents of the map that backs the state with an override, so instead we use + // the public API once again. as a consequence, this is not a pure unit test of a single method, but it does verify + // the expected behavior when looking at set and get as one unit. + describe('#set and #get', () => { + it('should store conversation state', async () => { + // Arrange + const dummyConversationState = Symbol(); + const dummyConversationId = 'CONVERSATION_ID'; + const { MemoryStore } = await importConversationStore(); + + // Act + const store = new MemoryStore(); + await store.set(dummyConversationId, dummyConversationState); + const actualConversationState = await store.get(dummyConversationId); + + // Assert + assert.equal(actualConversationState, dummyConversationState); + }); + + it('should reject lookup of conversation state when the conversation is not stored', async () => { + // Arrange + const { MemoryStore } = await importConversationStore(); + + // Act + const store = new MemoryStore(); + try { + await store.get('CONVERSATION_ID'); + assert.fail(); + } catch (error) { + // Assert + assert.instanceOf(error, Error); + assert.notInstanceOf(error, AssertionError); + } + }); + + it('should reject lookup of conversation state when the conversation is expired', async () => { + // Arrange + const dummyConversationId = 'CONVERSATION_ID'; + const dummyConversationState = Symbol(); + const expiresInMs = 5; + const { MemoryStore } = await importConversationStore(); + + // Act + const store = new MemoryStore(); + await store.set(dummyConversationId, dummyConversationState, Date.now() + expiresInMs); + await delay(expiresInMs * 2); + try { + await store.get(dummyConversationId); + assert.fail(); + } catch (error) { + // Assert + assert.instanceOf(error, Error); + assert.notInstanceOf(error, AssertionError); + } + }); + }); +}); + /* Testing Harness */ type MiddlewareArgs = AnyMiddlewareArgs & { next: NextMiddleware, context: Context }; diff --git a/src/test-helpers.ts b/src/test-helpers.ts index e97bf8bc2..f444ecc45 100644 --- a/src/test-helpers.ts +++ b/src/test-helpers.ts @@ -53,3 +53,9 @@ export function createFakeLogger(): FakeLogger { error: sinon.fake(), }; } + +export function delay(ms: number = 0): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}