npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

declarative-nock

v1.1.0

Published

Declarative Nock

Downloads

58

Readme

nlm-github nlm-node nlm-version

declarative-nock

An opinionated package to provide an easier-to-use declarative syntax in front of nock, allowing OpenAPI-like syntax for the route paths.

Install

npm i declarative-nock

Example

const { DeclarativeNock } = require('declarative-nock');
const { expect } = require('chai');
const client = require('@example/some-client');

const GET_PET = 'get /v2/pet/{petId}';
const ADD_PET = 'post /v2/pet/{petId}';
const GIST = 'post /gist';

const dn = new DeclarativeNock({
  petStore: {
    url: 'https://petstore.swagger.io',
    state: { petName: 'Fluffers' },
    mocks: {
      [GET_PET]: ({ state: { petName: name }, params: { id } }) => ({
        body: { id, name }
      }),
      [ADD_PET]: [
        { statusCode: 500 },
        { body: { id: 42 } }
      ]
    },
  },
  github: {
    url: 'https://api.github.com',
    state: { gistStatus: 201 },
    mocks: {
      [GIST]: ({ state: { gistStatus: statusCode } }) => ({
        statusCode,
        body: { id: 'abc123' },
        headers: { Location: 'https://api.github.com/gists/abc123' },
      }),
    }
  }
});
const { github, petStore } = dn.origins;

describe('the tests', () => {
  dn.addMochaHooks();

  it('makes a gist for the default name successfully', async () => {
    await client.uploadGistOfRandomPet();

    expect(github.one(GIST))
      .to.have.nested.property('body.files.pet', 'Fluffers');
    expect(petStore.all(GET_PET)).to.have.lengthOf(1);
  });

  it('fails for a bad name', async () => {
    petStore.setState({ petName: 'Grumpers' });
    github.setState({ gistStatus: 500 });

    await expect(client.uploadGistOfRandomPet())
      .to.eventually.be.rejectedWith('Got 500 for Grumpers');
  });

  it('retries on 500 adding a pet', async () => {
    await client.addPet('Kirby');

    expect(petStore.all(ADD_PET)).to.have.lengthOf(2);
  })
});

API

class DeclarativeNock

class DeclarativeNock {
  constructor(originDecl: { [originId: string]: OriginDecl });
  origins: { [originId: string]: Origin };
  nock: Nock;
  installMocks(): void;
  removeMocks(): void;
  resetAll(): void;
  addMochaHooks(opts?: { disableNetConnect: boolean = true }): void;
  addJestHooks(opts?: { disableNetConnect: boolean = true }): void;
}

The main external interface to declarative-nock is the DeclarativeNock class, whose constructor takes your declared mocks for one or more origins, and returns a DeclarativeNock instance to manage your mocks. Note that this does not install the http interceptors until you call .installMocks()

origins

All the origin-specific methods and data, keyed by the same originId you chose when you gave the origin declarations in the constructor.

installMocks()

Installs the nock interceptors: you often only want to do this when the test suite starts, so that require()ing your test file doesn't have side effects.

removeMocks()

The same as calling .nock.cleanAll() - removes ALL registered interceptors, not just those registered in this call.

resetAll()

Is the equivalent of resetting all state to its initial starting conditions, including:

  • forgets about all received requests (.resetReceived())
  • resets multi-reply sections to their first (.resetMultiReplies())
  • Undo overrides added with .setState() (.resetState())

addMochaHooks({ disableNetConnect = true })

Makes the following calls for mocha-compatible convenience to be called inside a describe():

before(() => dn.installMocks());
after(() => dn.removeMocks());
afterEach(() => dn.resetAll());

Additionally, if the disableNetConnect option is true (the default), then the before()/after() will disable and re-enable net connect using .nock.disable/enableNetConnect() for you. If you don't wish this behavior, set disableNetConnect: false.

addJestHooks({ disableNetConnect = true })

Exactly like addMochaHooks() except calls beforeAll() and afterAll() instead of before() and after() to work with Jest.

nock

A reference to require('nock') so you don't have to explicitly include it as a dependency; useful for things like nock.restore/active(), nock.disable/enableNetConnect(), and nock.cleanAll() (Though many of these are handled for you by .addMochaHooks())

class Origin

class Origin {
  scope?: Nock.Scope;
  setState(overrides: object): void;
  one?(methodPath: string): RequestObj;
  all?(methodPath: string): RequestObj[];
  resetReceived(): void;
  resetMultiReplies(): void;
  resetState(): void;
};

setState(object)

Sets override to the default state given as state: { ... } in the OriginDecl. This is useful for supplying some "backchannel" changes to a mock for testing alternative behavior. The changes given will be shallow-merged onto the state defaults.

one(methodPath)

Will assert that, since the last resetState/All() call, exactly one request to the given endpoint was received, and returns its RequestObj (including headers).

(Will not exist if recordRequests: false was set in the OriginDecl)

all(methodPath)

Returns an array of the zero or more requests to the given endpoint since the last reset.

(Will not exist if recordRequests: false was set in the OriginDecl)

resetReceived()

Will make .all() for all method paths return [] again.

resetState()

Erase overrides added by .setState()

resetMultiReplies()

If you supplied an array for Reply, and requests have been made to it, this will reset it to using the first reply again.

scope

A reference to the nock scope returned by invoking nock() for you - only exists once you have called .installMocks()

Types

OriginDecl

type OriginDecl = { 
  url: string | RegExp, 
  mocks: Mocks, 
  prefix?: string | RegExp, 
  state?: object, 
  recordRequests?: boolean 
}

url

Url for the host to mock, per specifying hostname in the nocks docs.

mocks

The actual declared mocks for this origin.

prefix

An optional prefix - if your url is a string, you may simply append this to the URL instead, e.g.: https://myhost.example.com/some/prefix. If your url is a RegExp, however, or you wish for this prefix itself to be a RegExp, you can specify it here. For example, to handle public & internal GitHub with the same mocks, you could do:

{
  url: /^https:\/\/(?:api\.github\.com|github\.example\.com)/,
  prefix: /(?:\/api\/v3)?/, // optional prefix for the internal server
  // ...
}

state

If given, defaults for the state property which will be available in dynamic Reply functions - intended to be overridden using .setState().

recordRequests

Whether received requests should be recorded to validate in your tests - defaults to true. If you have a particularly busy API that would use up too much memory to record this during a run, you can disable this.

RequestObj

type RequestObj = { 
  params: PathParams, 
  query: QueryArgs, 
  body: string | object, 
  headers?: Headers, 
  state?: object 
}

The RequestObj is used in two contexts:

When passed to a dynamic mock Reply function, it will not have headers, a state object containing any test state vars with their overrides will be available, and the body will be either a string or, if the Content-Type header appears to be JSON, an object decoded from the JSON.

When checked after the request is made using .reqs.one/all(), headers will also be available.

Mocks

type Mocks = { [methodPath: string]: Reply | Reply[] }

The mocks object you pass has as its properties strings of the form: "${method} ${routePath}", where method is one of: get, post, etc., and routePath is an OpenAPI-compatible path. Examples are get / or post /some/{id}

One variance is that you may include exactly one final path component like /* which will place all remaining path components into params[0], similar to the express behavior with named path params. See the PathParams docs below.

If the reply is an array, then the multiple replies will be used in the following manner:

  1. Each request will use the next reply in the list
  2. If more requests and available replies are made, the last available Reply will be re-used

If you wish to have more-requests-than-replies to be an error condition, just do something like:

{
  'get /only-two': [
    { body: { id: 'first' } },
    { body: { id: 'second' } },
    () => {
      throw new Error('Should only have received 2 requests');
    }
  ]
}

Reply

type Reply = ResponseObj | (RequestObj) => ResponseObj|Promise<ResponseObj>

OK, so that's a big type sig for Reply - let's break down the options:

  1. Just a ResponseObj - this is statically returned every time
  2. A function which receives as arguments descriptive details of the attempted request, and should return either a ResponseObj or a Promise for one. The body will NOT be decoded (JSON will be a JSON string, e.g.), and binary request bodies will be a hex-encoded string, which can be decoded with Buffer.from(body, 'hex')

ResponseObj

type ResponseObj = { 
  statusCode?: number, 
  headers?: Headers, 
  body?: ResponseBody 
}
  • statusCode: defaults to 200
  • headers: for defaults see below
  • body: defaults to ''

PathParams

type PathParams = { [param: string]: string }

For each {param} section present in the path, the supplied value will be be available here, so e.g. get /info/{user}/{id}, if requested as GET /info/arthur/42 would produce { params: { user: 'arthur', id: '42' } }

The single wildcard will place its string into [0], so e.g.: get /{id}/contents/* with a request of GET /42/contents/x/y/z would produce: { params: { id: '42', '0': 'x/y/z' } }

QueryArgs

type QueryArgs = { [arg: string]: string | string[] }

The query section of the requested URI, if present, will be converted into an object by using the builtin querystring library.

Headers

type Headers = { [headerName: string]: string }

Any response headers to send. Content-Type will default to application/json

ResponseBody

type ResponseBody = object | string | Buffer
  • If the response body is an object, it will be JSON encoded.
  • If the response body is a string or Buffer it will be sent as-is