Skip to content
This repository has been archived by the owner on Feb 20, 2024. It is now read-only.

initial support for mockRule #95

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
RequestSerializer,
} from './http-serializer';
import { RecordMode as Mode } from './recording';
import Rule from './rule';

export interface IRedactProp {
property: string | string[];
Expand All @@ -30,6 +31,7 @@ export interface IResponseForMatchingRequest {
*/
export default class Context {
public mode: Mode = Mode.Spy;
public rules: Rule[] = [];

/**
* Setting to redact all incoming requests to match redacted mocks
Expand Down
2 changes: 2 additions & 0 deletions src/filtering/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export type UnsafeMatchFn = (serialized: ISerializedRequestResponseToMatch) => b

export type Matcher = ISerializedHttpPartialDeepMatch | MatchFn;

export type HttpFilter = string | RegExp | Matcher;

export const EMPTY_RESPONSE = { body: {}, headers: {}, statusCode: 0 };

/**
Expand Down
62 changes: 62 additions & 0 deletions src/rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Context from './context';
import { YesNoError } from './errors';
import { Matcher } from './filtering/matcher';
import MockResponse from './mock-response';

export enum MockMode {
kevinmstephens marked this conversation as resolved.
Show resolved Hide resolved
Live = 'LIVE',
Record = 'RECORD',
Respond = 'RESPOND',
}

export interface IRule {
matcher: Matcher;
mock?: MockResponse;
mode: MockMode;
kevinmstephens marked this conversation as resolved.
Show resolved Hide resolved
}

export interface IRuleParams {
context: Context;
matcher: Matcher;
}

export default class Rule implements IRule {
public matcher: Matcher;
public mock?: MockResponse;
public mode: MockMode;
private readonly ctx: Context;

constructor({ context, matcher = {} }: IRuleParams) {
this.ctx = context;
this.matcher = matcher;
this.mode = MockMode.Record;
}

/**
* Set the rule mode to 'record'
*/
public record(): IRule {
const index = this.ctx.rules.length - 1;

if (index < 0) {
throw new YesNoError('No rules have been defined yet');
}

this.ctx.rules[index].mode = MockMode.Record;
return this.ctx.rules[index];
}

/**
* Set the rule mode to 'live'
*/
public live(): IRule {
const index = this.ctx.rules.length - 1;

if (index < 0) {
throw new YesNoError('No rules have been defined yet');
}

this.ctx.rules[index].mode = MockMode.Live;
return this.ctx.rules[index];
}
}
100 changes: 65 additions & 35 deletions src/yesno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { YesNoError } from './errors';
import * as file from './file';
import FilteredHttpCollection, { IFiltered } from './filtering/collection';
import { ComparatorFn } from './filtering/comparator';
import { ISerializedHttpPartialDeepMatch, MatchFn } from './filtering/matcher';
import { HttpFilter, ISerializedHttpPartialDeepMatch, match, MatchFn } from './filtering/matcher';
import { redact as redactRecord, Redactor } from './filtering/redact';
import {
createRecord,
Expand All @@ -22,14 +22,13 @@ import {
import Interceptor, { IInterceptEvent, IInterceptOptions, IProxiedEvent } from './interceptor';
import MockResponse from './mock-response';
import Recording, { RecordMode as Mode } from './recording';
import Rule, { MockMode } from './rule';

const debug: IDebugger = require('debug')('yesno');

export type GenericTest = (...args: any) => Promise<any> | void;
export type GenericTestFunction = (title: string, fn: GenericTest) => any;

export type HttpFilter = string | RegExp | ISerializedHttpPartialDeepMatch | MatchFn;

export interface IRecordableTest {
test?: GenericTestFunction;
it?: GenericTestFunction;
Expand Down Expand Up @@ -77,6 +76,19 @@ export class YesNo implements IFiltered {
this.setMode(Mode.Spy);
}

/**
* Set rule for mock/record
*
* @param filter to match requests
* @return new rule index
*/
public mockRule(filter: HttpFilter): Rule {
const matcher = _.isString(filter) || _.isRegExp(filter) ? { url: filter } : filter;
const rule = new Rule({ context: this.ctx, matcher });
this.ctx.rules.push(rule);
return rule;
}

/**
* Mock responses for intercepted requests
* @todo Reset the request counter?
Expand Down Expand Up @@ -155,6 +167,7 @@ export class YesNo implements IFiltered {

return records;
}

/**
* Save intercepted requests
*
Expand Down Expand Up @@ -286,6 +299,54 @@ export class YesNo implements IFiltered {
private async onIntercept(event: IInterceptEvent): Promise<void> {
this.recordRequest(event.requestSerializer, event.requestNumber);

const sendMockResponse = async () => {
try {
const mockResponse = new MockResponse(event, this.ctx);
const sent = await mockResponse.send();

if (sent) {
// redact properties if needed
if (this.ctx.autoRedact !== null) {
const properties = _.isArray(this.ctx.autoRedact.property)
? this.ctx.autoRedact.property
: [this.ctx.autoRedact.property];
const record = createRecord({
duration: 0,
request: sent.request,
response: sent.response,
});
sent.request = redactRecord(record, properties, this.ctx.autoRedact.redactor).request;
}

this.recordResponse(sent.request, sent.response, event.requestNumber);
} else if (this.isMode(Mode.Mock)) {
throw new Error('Unexpectedly failed to send mock respond');
}
} catch (e) {
if (!(e instanceof YesNoError)) {
debug(`[#${event.requestNumber}] Mock response failed unexpectedly`, e);
e.message = `YesNo: Mock response failed: ${e.message}`;
} else {
debug(`[#${event.requestNumber}] Mock response failed`, e.message);
}

event.clientRequest.emit('error', e);
}
};

// process the set of defined rules
for (const rule of this.ctx.rules) {
// see if the rule matches
const matchFound = match(rule.matcher)({ request: event.requestSerializer });
if (matchFound) {
if (rule.mode === MockMode.Live) {
return event.proxy();
}
// check for a matching mock
return sendMockResponse();
}
}

if (!this.ctx.hasResponsesDefinedForMatchers() && !this.isMode(Mode.Mock)) {
// No need to mock, send event to its original destination
return event.proxy();
Expand All @@ -296,38 +357,7 @@ export class YesNo implements IFiltered {
return event.proxy();
}

try {
const mockResponse = new MockResponse(event, this.ctx);
const sent = await mockResponse.send();

if (sent) {
// redact properties if needed
if (this.ctx.autoRedact !== null) {
const properties = _.isArray(this.ctx.autoRedact.property)
? this.ctx.autoRedact.property
: [this.ctx.autoRedact.property];
const record = createRecord({
duration: 0,
request: sent.request,
response: sent.response,
});
sent.request = redactRecord(record, properties, this.ctx.autoRedact.redactor).request;
}

this.recordResponse(sent.request, sent.response, event.requestNumber);
} else if (this.isMode(Mode.Mock)) {
throw new Error('Unexpectedly failed to send mock respond');
}
} catch (e) {
if (!(e instanceof YesNoError)) {
debug(`[#${event.requestNumber}] Mock response failed unexpectedly`, e);
e.message = `YesNo: Mock response failed: ${e.message}`;
} else {
debug(`[#${event.requestNumber}] Mock response failed`, e.message);
}

event.clientRequest.emit('error', e);
}
sendMockResponse();
}

private onProxied({ requestSerializer, responseSerializer, requestNumber }: IProxiedEvent): void {
Expand Down