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

stubborn-ws

v6.2.0

Published

Web server to mock external HTTP APIs in tests

Downloads

4,276

Readme

Stubborn

Build Status Coverage Status code style: prettier node

Stubborn web server to mock external api responses. It is basically nock meets Dyson. Stubborn will strictly match the requests based on the definition like nock but in a separate web server like Dyson.

Node Support Policy

We will always support at least the latest Long-Term Support version of Node, but provide no promise of support for older versions. The supported range will always be defined in the engines.node property of the package.json of our packages.

We specifically limit our support to LTS versions of Node, not because this package won't work on other versions, but because we have a limited amount of time, and supporting LTS offers the greatest return on that investment.

It's possible this package will work correctly on newer versions of Node. It may even be possible to use this package on older versions of Node, though that's more unlikely as we'll make every effort to take advantage of features available in the oldest LTS version we support.

As new Node LTS versions become available we may remove previous versions from the engines.node property of our package's package.json file. Removing a Node version is considered a breaking change and will entail the publishing of a new major version of this package. We will not accept any requests to support an end-of-life version of Node. Any merge requests or issues supporting an end-of-life version of Node will be closed.

We will accept code that allows this package to run on newer, non-LTS, versions of Node. Furthermore, we will attempt to ensure our own changes work on the latest version of Node. To help in that commitment, our continuous integration setup runs against all LTS versions of Node in addition the most recent Node release; called current.

JavaScript package managers should allow you to install this package with any version of Node, with, at most, a warning if your version of Node does not fall within the range specified by our node engines property. If you encounter issues installing this package, please report the issue to your package manager.

Installation

npm install --save-dev stubborn-ws

or

yarn add -D stubborn-ws

Usage

Stubborn is a testing tool that let you hot load and unload routes into a webserver. Requests are strictly matched against routes definitions based on Method, Path, Query parameters, Headers and Body. If the request does not exactly match one route definition (ex: extra parameter, missing parameter, value does not match, etc), Stubborn will respond with a 501.

The very fact that Stubborn responds to the request validates that the parameters sent are the expected one, any change in the code that send the request will break the test. Any breaking change will be picked up by your test.

Stubborn response headers and body can be hardcoded or defined using a template.

You can find a complete working test suite of the following examples here.

import got from 'got';
import { Stubborn, STATUS_CODES, WILDCARD } from 'stubborn-ws';

describe('Test', () => {
  const sb = new Stubborn();

  beforeAll(async () => await sb.start());
  afterAll(async () => await sb.stop());

  // Clean up all routes after a test if needed
  afterEach(() => sb.clear());

  it('should respond to query', async () => {
    const body = { some: 'body' };
    sb.get('/').setResponseBody({ some: 'body' });

    const res = await request(`/`);

    expect(res.statusCode).toBe(STATUS_CODES.SUCCESS);
    expect(res.body).toEqual(body);
  });

  function request(path = '/', options = {}) {
    return got(`${sb.getOrigin()}${path}`, {
      method: 'GET',
      responseType: 'json',
      throwHttpErrors: false,
      ...options,
    });
  }
});

Stubborn strictly matches the request against the route definition.

If a query parameter or a header is missing, stubborn will return a 501 (not implemented)

it('should respond 501 if a parameter is missing', async () => {
  sb.get('/').setQueryParameters({ page: '1' });

  const res = await request(`/`);

  expect(res.statusCode).toEqual(STATUS_CODES.NOT_IMPLEMENTED);
});

If a query parameter or a header is added, stubborn will return a 501 (not implemented)

it('should respond 501 if a parameter is added', async () => {
  sb.get('/').setQueryParameters({ page: '1' });

  const res = await request(`/?page=1&limit=10`);

  expect(res.statusCode).toEqual(STATUS_CODES.NOT_IMPLEMENTED);
});

If a query parameter or a header does not match the route definition, stubborn will return a 501 (not implemented)

it('should respond 501 if a parameter does not match the definition', async () => {
  sb.get('/').setQueryParameters({ page: '1' });

  const res = await request(`/?page=2`);

  expect(res.statusCode).toEqual(STATUS_CODES.NOT_IMPLEMENTED);
});

You can use regex to match a parameter, header or body

it('should match using a regexp', async () => {
  sb.post('/', {
    slug: /^[a-z\-]*$/,
  });

  const res = await request(`/?page=2`, {
    method: 'POST',
    json: { slug: 'stubborn-ws' },
  });

  expect(res.statusCode).toEqual(200);
});

You can use a function to match a parameter, header or body

import { STATUS_CODES } from 'stubborn-ws';
it('should match using a function', async () => {
  sb.get('/').setQueryParameters({
    page: value => parseInt(value as string) > 0,
  });

  const res = await request(`/?page=2`);

  expect(res.statusCode).toBe(STATUS_CODES.SUCCESS);
});

Although this is not advised, you can use the WILDCARD constant to match any values:

import { WILDCARD } from 'stubborn-ws';
it('should match using wildcard', async () => {
  sb.get('/').setQueryParameters({ page: WILDCARD }).setHeaders(WILDCARD);

  const res = await request(`/?page=2`, {
    headers: { 'x-api-key': 'api key', 'any-other-header': 'stuff' },
  });

  expect(res.statusCode).toEqual(STATUS_CODES.SUCCESS);
});

Public API

See the API documentation

FAQ

Q: Stubborn is not matching my route definition and always return a 501

Stubborn is STUBBORN, therefore it will return a 501 if it does not exactly match the route definition you have set up. To help you find what missing in the route definition, you can compare it to the response body returned when receiving a 501 using the logDiff() method of a route:

const route = sb
  .get('/')
  // This header definition will miss additional header added by got, like user-agent, connexion, etc...
  .setHeaders({ 'X-Api-Key': 'test' })
  // Will log in console the diff between the route and any request throwing a 501
  .logDiffOn501();

const res = await request(sb.getOrigin(), {
  headers: { 'x-api-key': 'api key' },
});

expect(res.statusCode).toBe(501);

Q: How do I know if stubborn has been called and matched the route defined?

Stubborn will return a 501 (Not Implemented) if it received a request but cannot match any route. If the request matches the route it will respond according to the route response configuration and update the call property of the route

  async function call() {
    return request(sb.getOrigin());
  }

  // No route setup in Stubborn
  const res = await call();

  expect(res.statusCode).toBe(501);
  expect(res.body).toEqual({
    method: 'GET'
    path: '/',
    headers: {
      // ...
    }
    // ...
  });


  const route = sb.get('/')
    .setHeaders(null)
    .setResponseBody('content');


  const res = await call();
  expect(res.calls.length).toBe(1);
  expect(res.calls[0]).toEqual({
    method: 'GET'
    path: '/',
    headers: {
      // ...
    }
      // ...
    });

Q: Can I send the same request multiple times and have different response?

Stubborn returns the first route that match a request even if multiple routes could match that request. Using Route.removeRouteAfterMatching you can tell stubborn to remove a route from the router, and if another route matching then it will be used.

// First return a 400
sb.addRoute(
  new Route(METHODS.GET, '/')
    .setResponseStatusCode(400)
    .removeRouteAfterMatching({ times: 1 }), // Match one time then remove
);

// Then return a 500
sb.addRoute(
  new Route(METHODS.GET, '/')
    .setResponseStatusCode(500)
    .removeRouteAfterMatching({ times: 1 }), // Match one time then remove
);

// Finally always return 200
sb.addRoute(
  new Route(METHODS.GET, '/').setResponseStatusCode(200),
);

// First call match the first route, then the route is removed
expect(await httpClient.request({ path: '/' })).toReplyWith({
  status: 400,
});

// Second call match the second route, then the route is removed
expect(await httpClient.request({ path: '/' })).toReplyWith({
  status: 500,
});

// Any subsequent calls match the last route which is never removed
expect(await httpClient.request({ path: '/' })).toReplyWith({
  status: 200,
});

expect(await httpClient.request({ path: '/' })).toReplyWith({
  status: 200,
});