pact-foundation/pact-js

View on GitHub
src/dsl/interaction.ts

Summary

Maintainability
A
0 mins
Test Coverage
/**
 * An Interaction is where you define the state of your interaction with a Provider.
 * @module Interaction
 */

import { isNil, keys } from 'lodash';
import { reject } from 'ramda';
import { HTTPMethods, HTTPMethod } from '../common/request';
import { Matcher, isMatcher, AnyTemplate } from './matchers';
import ConfigurationError from '../errors/configurationError';

interface QueryObject {
  [name: string]: string | Matcher<string> | string[];
}
export type Query = string | QueryObject;

export type Headers = {
  [header: string]: string | Matcher<string> | (Matcher<string> | string)[];
};

export interface RequestOptions {
  method: HTTPMethods | HTTPMethod;
  path: string | Matcher<string>;
  query?: Query;
  headers?: Headers;
  body?: AnyTemplate;
}

export interface ResponseOptions {
  status: number;
  headers?: Headers;
  body?: AnyTemplate;
}

export interface InteractionObject {
  state: string | undefined;
  uponReceiving: string;
  withRequest: RequestOptions;
  willRespondWith: ResponseOptions;
}

export interface InteractionState {
  providerState?: string;
  description?: string;
  request?: RequestOptions;
  response?: ResponseOptions;
}

export interface InteractionStateComplete {
  providerState?: string;
  description: string;
  request: RequestOptions;
  response: ResponseOptions;
}

/**
 * Returns valid if object or matcher only contains string values
 * @param query
 */
const throwIfQueryObjectInvalid = (query: QueryObject) => {
  if (isMatcher(query)) {
    return;
  }

  Object.values(query).forEach((value) => {
    if (
      !(isMatcher(value) || Array.isArray(value) || typeof value === 'string')
    ) {
      throw new ConfigurationError(`Query must only contain strings.`);
    }
  });
};

export class Interaction {
  public state: InteractionState = {};

  /**
   * Gives a state the provider should be in for this interaction.
   * @param {string} providerState - The state of the provider.
   * @returns {Interaction} interaction
   */
  public given(providerState: string): this {
    if (providerState) {
      this.state.providerState = providerState;
    }

    return this;
  }

  /**
   * A free style description of the interaction.
   * @param {string} description - A description of the interaction.
   * @returns {Interaction} interaction
   */
  public uponReceiving(description: string): this {
    if (isNil(description)) {
      throw new ConfigurationError(
        'You must provide a description for the interaction.'
      );
    }
    this.state.description = description;

    return this;
  }

  /**
   * The request that represents this interaction triggered by the consumer.
   * @param {Object} requestOpts
   * @param {string} requestOpts.method - The HTTP method
   * @param {string} requestOpts.path - The path of the URL
   * @param {string} requestOpts.query - Any query string in the interaction
   * @param {Object} requestOpts.headers - A key-value pair oject of headers
   * @param {Object} requestOpts.body - The body, in {@link String} format or {@link Object} format
   * @returns {Interaction} interaction
   */
  public withRequest(requestOpts: RequestOptions): this {
    if (isNil(requestOpts.method)) {
      throw new ConfigurationError('You must provide an HTTP method.');
    }

    if (keys(HTTPMethods).indexOf(requestOpts.method.toString()) < 0) {
      throw new ConfigurationError(
        `You must provide a valid HTTP method: ${keys(HTTPMethods).join(', ')}.`
      );
    }

    if (isNil(requestOpts.path)) {
      throw new ConfigurationError('You must provide a path.');
    }

    if (typeof requestOpts.query === 'object') {
      throwIfQueryObjectInvalid(requestOpts.query);
    }

    this.state.request = reject(isNil, requestOpts) as RequestOptions;

    return this;
  }

  /**
   * The response expected by the consumer.
   * @param {Object} responseOpts
   * @param {string} responseOpts.status - The HTTP status
   * @param {string} responseOpts.headers
   * @param {Object} responseOpts.body
   * @returns {Interaction} interaction
   */
  public willRespondWith(responseOpts: ResponseOptions): this {
    if (
      isNil(responseOpts.status) ||
      responseOpts.status.toString().trim().length === 0
    ) {
      throw new ConfigurationError('You must provide a status code.');
    }

    this.state.response = reject(isNil, {
      body: responseOpts.body,
      headers: responseOpts.headers || undefined,
      status: responseOpts.status,
    }) as ResponseOptions;
    return this;
  }

  /**
   * Returns the interaction object created.
   * @returns {Object}
   */
  public json(): InteractionStateComplete {
    if (isNil(this.state.description)) {
      throw new ConfigurationError(
        'You must provide a description for the Interaction'
      );
    }
    if (
      isNil(this.state.request) ||
      isNil(this.state?.request?.method) ||
      isNil(this.state?.request?.path)
    ) {
      throw new ConfigurationError(
        'You must provide a request with at least a method and path for the Interaction'
      );
    }
    if (isNil(this.state.response) || isNil(this.state?.response?.status)) {
      throw new ConfigurationError(
        'You must provide a response with a status for the Interaction'
      );
    }

    return this.state as InteractionStateComplete;
  }
}

export const interactionToInteractionObject = (
  interaction: InteractionStateComplete
): InteractionObject => ({
  state: interaction.providerState,
  uponReceiving: interaction.description,
  withRequest: {
    method: interaction.request?.method,
    path: interaction.request?.path,
    query: interaction.request?.query,
    body: interaction.request?.body,
    headers: interaction.request?.headers,
  },
  willRespondWith: {
    status: interaction.response?.status,
    body: interaction.response?.body,
    headers: interaction.response?.headers,
  },
});