From c21b3a792e09169a7b8012355dbdc950a3cee21f Mon Sep 17 00:00:00 2001 From: "Michael P. Scott" Date: Fri, 7 Dec 2018 16:21:09 -0800 Subject: [PATCH] feat(#10) non-deterministic mocks --- src/context.ts | 29 +++++++++++++++++--- src/yesno.ts | 60 +++++++++++++++++++++++++++++++++++++---- test/unit/yesno.spec.ts | 25 +++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/context.ts b/src/context.ts index 3d9880b..44734a5 100644 --- a/src/context.ts +++ b/src/context.ts @@ -12,19 +12,40 @@ export default class Context { * B. Saved to disk if in record mode */ public interceptedRequestsCompleted: ISerializedHttp[] = []; - /** - * Serialized records loaded from disk. - */ - public loadedMocks: ISerializedHttp[] = []; /** * Proxied requests which have not yet responded. When completed * the value is set to "null" but the index is preserved. */ public inFlightRequests: Array = []; + /** + * Serialized records loaded from disk. + */ + private mocks: ISerializedHttp[] = []; + /** + * Mocks which have already been consumed. The ordinal position of + * each entry is a boolean that, when true, indicates the parallel + * array entry in loadedMocks has been consumed. + */ + private consumed: boolean[] = []; + public clear() { this.interceptedRequestsCompleted = []; this.inFlightRequests = []; this.loadedMocks = []; } + + public get consumedMocks(): boolean[] { + return this.consumed; + } + + public get loadedMocks(): ISerializedHttp[] { + return this.mocks; + } + public set loadedMocks(value: ISerializedHttp[]) { + this.mocks = value; + + // reset consumed mocks + this.consumed = []; + } } diff --git a/src/yesno.ts b/src/yesno.ts index b97bc16..b58ba18 100644 --- a/src/yesno.ts +++ b/src/yesno.ts @@ -282,6 +282,60 @@ export class YesNo implements IFiltered { }); } + /** + * Try to consume the mock. + * @param serializedRequest + * @param mock + * @param requestIndex + */ + private tryConsumeMock( + serializedRequest: ISerializedRequest, + mock: ISerializedHttp, + requestIndex: number, + ) { + if (this.ctx.consumedMocks[requestIndex]) { + return false; + } + + try { + // compare the request and the mock + comparator.byUrl(serializedRequest, mock.request, { requestIndex }); + + // if the comparator does not throw, then the mock is matching- mark it as consumed + this.ctx.consumedMocks[requestIndex] = true; + return true; + } catch (error) { + // if there is only one loaded mock, and it doesn't match, throw the detailed error + if (this.ctx.loadedMocks.length === 1) { + throw error; + } + + return false; + } + } + + /** + * Given a serialized request and request number, find the appropriately-matching mock. + * @param serializedRequest + * @param requestNumber + */ + private findMatchingMock( + serializedRequest: ISerializedRequest, + requestNumber: number, + ): ISerializedHttp | undefined { + // first see if the mock in the ordinal position can be consumed + const ordinalMock: ISerializedHttp = this.ctx.loadedMocks[requestNumber]; + if (this.tryConsumeMock(serializedRequest, ordinalMock, requestNumber)) { + return ordinalMock; + } + + // otherwise, try to consume the first matching mock + return this.ctx.loadedMocks.find( + (mock: ISerializedHttp, index: number): boolean => + this.tryConsumeMock(serializedRequest, mock, index), + ); + } + private async mockResponse({ clientRequest, interceptedRequest, @@ -294,16 +348,12 @@ export class YesNo implements IFiltered { await (readable as any).pipeline(interceptedRequest, requestSerializer); const serializedRequest = requestSerializer.serialize(); - const mock = this.ctx.loadedMocks[requestNumber]; + const mock = this.findMatchingMock(serializedRequest, requestNumber); if (!mock) { throw new YesNoError(`No mock found for request #${requestNumber}`); } - // Assertion must happen before promise - - // mitm does not support promise rejections on "request" event - comparator.byUrl(serializedRequest, mock.request, { requestIndex: requestNumber }); - const bodyString = _.isPlainObject(mock.response.body) ? JSON.stringify(mock.response.body) : mock.response.body; diff --git a/test/unit/yesno.spec.ts b/test/unit/yesno.spec.ts index 4efed98..d6e1c9d 100644 --- a/test/unit/yesno.spec.ts +++ b/test/unit/yesno.spec.ts @@ -182,6 +182,31 @@ describe('Yesno', () => { expect(yesno.intercepted()).to.have.lengthOf(1); }); + it('should scan for and use a mock just once', async () => { + yesno.mock([ + createMock(), + createMock({ + request: { + port: 4000, + }, + }), + createMock(), + ]); + + expect(yesno.intercepted()).to.have.lengthOf(0); + + // consumes first mock + await requestTestServer(); + + // consumes third mock + await requestTestServer(); + + expect(yesno.intercepted()).to.have.lengthOf(2); + + // fails because the remaining mock is not matching + await expect(mockedRequest()).to.be.rejectedWith(/YesNo: No mock found for request #2/); + }); + it('should reject a request for which no mock has been provided'); it('should handle unexpected errors');