Skip to content
This repository has been archived by the owner on Oct 30, 2022. It is now read-only.

Commit

Permalink
Extract UnmockFaker from Backend. (#355)
Browse files Browse the repository at this point in the history
* Add UnmockFaker.

* Remove some duplication.

* Simplify nockify.

* Move nock logic to ServiceStore.

* Clean-up.

* Add faker tests.

* Add type for NockAPI.

* Add docs.
  • Loading branch information
Kimmo Sääskilahti authored and Carolyn Stransky committed Dec 12, 2019
1 parent b805d8b commit c8ba193
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 68 deletions.
40 changes: 40 additions & 0 deletions packages/unmock-core/src/__tests__/faker.test.ts
@@ -0,0 +1,40 @@
import { u } from "..";
import UnmockFaker from "../faker";
import { ServiceStore } from "../service/serviceStore";

const serviceStore = new ServiceStore([]);
const faker = new UnmockFaker({ serviceStore });

const expectNServices = (expectedLength: number) =>
expect(Object.keys(faker.services).length).toEqual(expectedLength);

describe("UnmockFaker", () => {
beforeEach(() => serviceStore.removeAll());

describe("adding service with nock syntax", () => {
it("adds a service", () => {
faker
.nock("https://foo.com")
.get("/foo")
.reply(200, { foo: u.string() });
expectNServices(1);
});
it("adds a service and allows faking it", () => {
faker
.nock("https://foo.com")
.get("/foo")
.reply(200, { foo: u.string() });
const res = faker.fake({
host: "foo.com",
protocol: "https",
method: "get",
path: "/foo",
pathname: "/foo",
headers: {},
query: {},
});
expect(res).toHaveProperty("body");
expect(res.body).toBeDefined();
});
});
});
34 changes: 16 additions & 18 deletions packages/unmock-core/src/backend/index.ts
@@ -1,6 +1,6 @@
import debug from "debug";
import * as _ from "lodash";
import { responseCreatorFactory } from "../generator";
import UnmockFaker from "../faker";
import { IInterceptor, IInterceptorFactory } from "../interceptor";
import {
CreateResponse,
Expand Down Expand Up @@ -78,10 +78,11 @@ const NoopServiceDefLoader: IServiceDefLoader = {
};

export class Backend {
public serviceStore: ServiceStore = new ServiceStore([]);
public readonly serviceStore: ServiceStore = new ServiceStore([]);
public readonly interceptorFactory: IInterceptorFactory;
public readonly serviceDefLoader: IServiceDefLoader;
public readonly randomNumberGenerator: IRandomNumberGenerator;
public faker: UnmockFaker;
public handleRequest?: OnSerializedRequest;
protected readonly requestResponseListeners: IListener[];
private interceptor?: IInterceptor;
Expand All @@ -97,16 +98,18 @@ export class Backend {
this.serviceDefLoader = serviceDefLoader || NoopServiceDefLoader;
this.randomNumberGenerator = rng || randomNumberGenerator({});
this.loadServices();
this.faker = new UnmockFaker({ serviceStore: this.serviceStore });
}

public get services(): ServiceStoreType {
return (this.serviceStore && this.serviceStore.services) || {};
return this.serviceStore.services;
}

/**
* Start the interceptor.
*
* @param options
* @returns `states` object, with which one can modify states of various services.
*
*/
public initialize(options: IUnmockOptions) {
if (process.env.NODE_ENV === "production" && !options.useInProduction()) {
Expand All @@ -118,14 +121,14 @@ export class Backend {
this.interceptor = undefined;
}

const createResponse = responseCreatorFactory({
listeners: this.requestResponseListeners,
options,
rng: this.randomNumberGenerator,
store: this.serviceStore,
});
/**
* Backward compatibility: Allow setting options at run-time when initializing unmock.
*/
this.faker.setOptions(options);

this.handleRequest = buildRequestHandler(createResponse);
this.handleRequest = buildRequestHandler(
this.faker.createResponse.bind(this.faker),
);

this.interceptor = this.interceptorFactory({
onSerializedRequest: this.handleRequest,
Expand All @@ -139,12 +142,7 @@ export class Backend {
this.interceptor = undefined;
}
this.handleRequest = undefined;
if (this.serviceStore) {
// TODO - this is quite ugly :shrug:
Object.values(this.serviceStore.services).forEach(service =>
service.reset(),
);
}
this.serviceStore.resetServices();
}

public loadServices(): void {
Expand All @@ -157,7 +155,7 @@ export class Backend {
ServiceParser.parse(serviceDef),
);

this.serviceStore = new ServiceStore(coreServices);
this.serviceStore.update(coreServices);
}
}

Expand Down
97 changes: 97 additions & 0 deletions packages/unmock-core/src/faker/index.ts
@@ -0,0 +1,97 @@
import * as _ from "lodash";
import { responseCreatorFactory } from "../generator";
import {
CreateResponse,
IListener,
ISerializedRequest,
ISerializedResponse,
IUnmockOptions,
ServiceStoreType,
} from "../interfaces";
import {
IRandomNumberGenerator,
randomNumberGenerator,
} from "../random-number-generator";
import { addFromNock, NockAPI, ServiceStore } from "../service/serviceStore";

export interface IFakerOptions {
listeners?: IListener[];
serviceStore: ServiceStore;
randomNumberGenerator?: IRandomNumberGenerator;
}

const DEFAULT_OPTIONS: IUnmockOptions = {
useInProduction: () => true,
isWhitelisted: (__: string) => false,
randomize: () => true,
log: (__: string) => {}, // tslint:disable-line:no-empty
};

export default class UnmockFaker {
public createResponse: CreateResponse;
/**
* Add a new service to the faker using `nock` syntax.
*/
public readonly nock: NockAPI;
private readonly serviceStore: ServiceStore;
private readonly randomNumberGenerator: IRandomNumberGenerator;
private readonly listeners: IListener[];
/**
* Unmock faker. Creates fake responses to fake requests, using
* the services contained in `serviceStore`.
* Add new services with the `faker.nock` method.
* @param options Options for creating the object.
*/
public constructor({
listeners,
randomNumberGenerator: rng,
serviceStore,
}: IFakerOptions) {
this.listeners = listeners ? listeners : [];
this.randomNumberGenerator = rng || randomNumberGenerator({});
this.serviceStore = serviceStore;
this.createResponse = this.createResponseCreator();
this.nock = addFromNock(this.serviceStore);
}

/**
* Create a new faker function from the given options.
* @param options Options for faking responses.
*/
public setOptions(options: IUnmockOptions) {
this.createResponse = this.createResponseCreator(options);
}

/**
* Fake a response to a request.
* @param request Serialized request.
* @throws Error if no matcher was found for the request.
* @returns Serialized response.
*/
public fake(request: ISerializedRequest): ISerializedResponse {
return this.createResponse(request);
}

/**
* Services dictionary mapping service name to `Service` object.
*/
public get services(): ServiceStoreType {
return (this.serviceStore && this.serviceStore.services) || {};
}

/**
* Reset the states of all services.
*/
public reset() {
this.serviceStore.resetServices();
}

private createResponseCreator(options?: IUnmockOptions): CreateResponse {
return responseCreatorFactory({
listeners: this.listeners,
options: options || DEFAULT_OPTIONS,
rng: this.randomNumberGenerator,
store: this.serviceStore,
});
}
}
50 changes: 15 additions & 35 deletions packages/unmock-core/src/index.ts
@@ -1,9 +1,10 @@
// Sinon for asserts and matchers
import * as sinon from "sinon";
import Backend, { buildRequestHandler } from "./backend";
import UnmockFaker from "./faker";
import { ILogger, IUnmockOptions, IUnmockPackage } from "./interfaces";
import { ExtendedJSONSchema, nockify, vanillaJSONSchemify } from "./nock";
import internalRunner, { IRunnerOptions } from "./runner";
import { addFromNock, NockAPI, ServiceStore } from "./service/serviceStore";
import { AllowedHosts, BooleanSetting, IBooleanSetting } from "./settings";
import * as typeUtils from "./utils";

Expand All @@ -25,6 +26,8 @@ export class UnmockPackage implements IUnmockPackage {
*/
public randomize: IBooleanSetting;
public readonly backend: Backend;
public readonly nock: NockAPI;
private readonly opts: IUnmockOptions;
private logger: ILogger = { log: () => undefined }; // Default logger does nothing
constructor(
backend: Backend,
Expand All @@ -38,16 +41,21 @@ export class UnmockPackage implements IUnmockPackage {
this.allowedHosts = new AllowedHosts();
this.useInProduction = new BooleanSetting(false);
this.randomize = new BooleanSetting(false);
}

public on() {
const opts: IUnmockOptions = {
this.opts = {
useInProduction: () => this.useInProduction.get(),
isWhitelisted: (url: string) => this.allowedHosts.isWhitelisted(url),
log: (message: string) => this.logger.log(message),
randomize: () => this.randomize.get(),
};
this.backend.initialize(opts);
this.nock = addFromNock(this.backend.serviceStore);
}

public newFaker(): UnmockFaker {
return new UnmockFaker({ serviceStore: new ServiceStore([]) });
}

public on() {
this.backend.initialize(this.opts);
return this;
}
public init() {
Expand All @@ -72,34 +80,6 @@ export class UnmockPackage implements IUnmockPackage {
return f;
}

public nock(
baseUrl: string,
nameOrHeaders?:
| string
| { reqheaders?: Record<string, ExtendedJSONSchema> },
name?: string,
) {
const internalName =
typeof nameOrHeaders === "string"
? nameOrHeaders
: typeof name === "string"
? name
: undefined;
const requestHeaders =
typeof nameOrHeaders === "object" && nameOrHeaders.reqheaders
? Object.entries(nameOrHeaders.reqheaders).reduce(
(a, b) => ({ ...a, [b[0]]: vanillaJSONSchemify(b[1]) }),
{},
)
: {};
return nockify({
backend: this.backend,
baseUrl,
requestHeaders,
name: internalName,
});
}

public associate(url: string, name: string) {
this.backend.serviceStore.updateOrAdd({ baseUrl: url, name });
}
Expand All @@ -109,6 +89,6 @@ export class UnmockPackage implements IUnmockPackage {
}

public reset() {
Object.values(this.backend.services).forEach(service => service.reset());
this.backend.serviceStore.resetServices();
}
}
7 changes: 3 additions & 4 deletions packages/unmock-core/src/nock.ts
Expand Up @@ -20,7 +20,6 @@ import {
import { fromTraversable, Iso, Prism } from "monocle-ts";
import { valAsConst } from "openapi-refinements";
import * as querystring from "query-string";
import Backend from "./backend";
import { identityGetter } from "./generator";
import { CodeAsInt, HTTPMethod } from "./interfaces";
import { Schema, ValidEndpointType } from "./service/interfaces";
Expand Down Expand Up @@ -706,13 +705,13 @@ const buildFluentNock = (
) as IFluentDynamicService);

export const nockify = ({
backend,
serviceStore,
baseUrl,
requestHeaders,
name,
}: {
backend: Backend;
serviceStore: ServiceStore;
baseUrl: string;
requestHeaders: Record<string, JSSTAnything<EJSEmpty, {}>>;
name?: string;
}) => buildFluentNock(backend.serviceStore, baseUrl, requestHeaders, name);
}) => buildFluentNock(serviceStore, baseUrl, requestHeaders, name);

0 comments on commit c8ba193

Please sign in to comment.