Skip to content

digio/oaat

Repository files navigation

oaat

Open API AWS Tool

npm package Build Status Coverage Status Downloads

Open API-spec AWS tool for recording, linting & comparing API responses; building and deploying a spec to AWS API Gateway.

Table of Contents

Installation

npm install -g oaat

# Display help 
oaat --help

Note: Node 12.3 or higher runtime required.

Usage

This tool does 5 things:

  • oaat record records API responses to requests specified in x-examples fields in an OpenAPI 3.x spec file.
  • oaat lint lints an OpenAPI 3.x spec file (basic formatting; tools like speccy provide more capability, but don't do formatting).
  • oaat build creates an OpenAPI 3.x spec file with API Gateway headers, optionally with mock responses for the APIs.
  • oaat compare compares the earlier-recorded responses to the last responses for endpoints in an OpenAPI 3.x spec file.
  • oaat validate validates that a spec-file is compliant with the OpenAPI 3.x specification.
  • (Coming soon) oaat deploy deploys an OpenAPI 3.x spec file that has the API Gateway headers to API Gateway.

Recording

This tool provides the capability to record responses by making requests to the real API endpoints (oaat record), and optionally use them as mock responses later (oaat deploy), by reading an Open API spec file (v3.x) in JSON format.

Command

$ oaat record --help
Usage: oaat record [options] <jsonFile> [serverUrl]

Record the responses of API spec file endpoint requests (optionally use a different server to make requests)

Options:
  -o, --output <file>          Output file (if different to jsonFile)
  -c, --config <file>          Config file to override default config
  -s, --sec-tokens <key=val,>  Pass security token(s) matching the "key" in spec.securitySchemes, with a "value"
  -q, --quiet                  No logging
  -v, --verbose                Verbose logging
  -d, --dry-run                Dry run (no changes made)
  -h, --help                   display help for command

To get valid example responses - to use for mocking & as documentation - we need to add some custom properties to the OpenAPI spec file.

path.method.responses.statusCode["x-examples"]

The x-examples object is a custom field that allows for the description of multiple examples of inputs, and stores the corresponding response (for use in mocks and testing).

x-examples is an object, with each child-property being the name of an example. There must be at-least one child-property example-name for x-examples.

Each example-object can have the following properties:

  • parameters - optional
  • requestBody - optional
  • responseFile - generated when recording a response

x-examples[exampleName].parameters

This property is required whenever an API has a non-empty path.method.parameters array. The elements in x-examples[example-name].parameters correspond to the elements in parameters. The order of each parameters object is significant.

Each parameters object has either a value or script property:

  • value can be any data type (string, number, array, object or null)
  • script is a reference to a JavaScript file. The JavaScript file must export a function which returns a value which can be used in the corresponding parameters argument. The function can be asynchronous. This is useful for situations where an endpoint relies on the result of another endpoint.

x-examples[exampleName].requestBody

This property is required whenever an API has a non-empty path.method.requestBody property. The requestBody object has either a value or script property - same as the x-examples[exampleName].parameters (above).

x-examples[exampleName].responseFile

The responseFile property points to a mock-response file.

When an API has no parameters, the responseFile property is generated for the 200 response automatically. For all other response codes, the responseFile property will need to be added manually, along with the mock file for that response.

Example

"paths": {
  "/posts/{id}": {
    "post": {
      "tags": ["posts"],
      "summary": "Get specific post",
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "description": "The ID of the post to retrieve",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "requestBody": {
        "description": "Optional description in *Markdown*",
        "required": true,
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Pet" 
            }
          }
        }
      },
      "responses": {
        "200": {
          "description": "successful operation",
          "content": {
            "*/*": {
              "schema": {
                "$ref": "#/components/schemas/Post"
              }
            }
          },
          "x-examples": {
            "id_1": {
              "parameters": [{ "value": "1" }],
              "requestBody": { "script": "scripts/getIdFromDatabase.js" }
            }
          }
        }
      }
    }
  }
}

In the above example, the POST /posts/{id} endpoint requires two parameters - a path parameter ({id}) and a requestBody parameter. The corresponding x-examples[exampleName] object has two properties: parameters, which uses the value property to specify the id parameter; the requestBody property uses the script property to specify a JS file which will produce the value for the requestBody parameter.

JS script example:

const queueWrapper = require('../src/queueWrapper');

/**
 * Returns the post data
 * @param {string} serverUrl
 * @param {string} path        E.g. "/post/{id}"
 * @return {Promise<*>}
 */
function doAsyncThing({ serverUrl }) {
  return new Promise(resolve => {
    setTimeout(() => resolve('async value 1'), 100);
  });
}

module.exports = queueWrapper(doAsyncThing);

The above JavaScript module exports an async function which returns an object asynchronously. The queueWrapper() function is there to combine multiple requests into a single request, in scenarios where multiple endpoint-examples require the same async-value.

Security tokens and headers

APIs are often protected with security tokens and headers. To call these APIs, the security token can be passed to the command line via -s securityHeaderType=123someValue,nextToken=nextValue,.... Alternatively, the security token values can be specified in the securitySchemes section of configuration file. By default, securitySchemes contains no keys.

oaat will always pick the first security scheme when there are multiple security schemes available for an endpoint.

Disabling endpoint recording

Sometimes it may be necessary to disable the recording of certain endpoints, while keeping the endpoint in the spec.

Disable entire endpoint (all responses)

Add the field paths[path][method].x-ignore with a value of true to disable the recording (and comparing) of this endpoint.

Disable a single endpoint response

Add the field paths[path][method].responses[status].x-ignore with a value of true to disable the recording (and comparing) of this endpoint response.

Ignoring changes to certain fields - path.method.responses.statusCode["x-test-ignore-paths"]

The x-test-ignore-paths property is an array of paths (in Lodash path format (see example)) to be ignored.

For example, the foo.correlationId property may change in every request. When comparing a new request against the mock-file, the objects will never match. To overcome this, we can use the x-test-ignore-paths property to ignore this field when comparing the response to the mock-file:

"paths": {
  "/foo/bar": {
    "get": {
      "summary": "Get foo's bars",
      "produces": ["application/json"],
      "parameters": [],
      "responses": {
        "200": {
          "schema": {
            "$ref": "#/definitions/Cart"
          },
          "x-test-ignore-paths": [
            "foo.correlationId"
          ]
        }
      }
    }
  }
}

Linting

There are lots of good tools that can check the syntax and style of Open API Sepc 3.x files:

oaat compliments these tools, as it lints things that the others don't:

  • sort paths alphabetically (true)
  • sort components.schemas alphabetically (true)
  • copy x-examples examples into parameter.examples and requestBody.examples

Command

$ oaat lint --help
Usage: oaat lint [options] <jsonFile> [serverUrl]

Tidy the API Spec up a bit

Options:
  -o, --output <file>          Output file (if different to jsonFile)
  -c, --config <file>          Config file to override default config
  -s, --sec-tokens <key=val,>  Pass security token(s) matching the "key" in spec.securitySchemes, with a "value"
  -q, --quiet                  no logging
  -v, --verbose                verbose logging
  -h, --help                   display help for command

Note: serverUrl and sec-tokens are only required when the config.lint.syncExamples is true.

See the configuration file for further options.

Building

Creates an OpenAPI 3.x spec file with API Gateway headers, optionally with mock responses for the APIs.

Command

$ oaat build --help
Usage: oaat build [options] <jsonFile> <outputJsonFile> [serverUrl]

Adds custom headers & Swagger UI endpoint to allow deployment of spec file to AWS API Gateway with documentation

Options:
  -c, --config <file>  Config file to override default config
  -m, --mock           Uses the recorded responses as mock responses
  -q, --quiet          No logging
  -v, --verbose        Verbose logging
  -d, --dry-run        Dry run (no changes made)
  -h, --help           display help for command

Mocking

API Gateway supports different kinds of integrations. One integration-type is "mock", whereby static responses are returned for any API requests. Mock integrations are useful for testing, documentation, or as a backup for the real API when things go wrong.

To create mock responses, add the x-mock-file property to each endpoint and specify the -m flag in the command.

When using the -m option, the schema, security and requestBody information is not present in the generated spec file. This is due to limitations within API Gateway's handling of specific Open API Spec 3.x features (removing the schemas makes most of the issues disappear). However, the Swagger UI still loads the original spec file, so that the documentation is correct.

path.method.responses.statusCode["x-mock-file"]

x-mock-file is either a string or an object, associating an endpoint with a response file.

When x-mock-file is a string, the value is a path to a response file, and the response-data is used regardless of the path-parameters supplied in the request.

When x-mock-file is an object, the property-key is the API-path that is generated, and the value is a path to a response file (as above). In this mode, it is possible to generate multiple mock responses by specifying multiple API paths as property keys (see example below).

Example
"paths": {
  "/posts/{id}": {
    "post": {
      "responses": {
        "200": {
          "description": "String example",
          "x-mock-file": "mock/foo.json"
        }
      }
    }
  },
  "/users/{id}": {
    "post": {
      "responses": {
        "200": {
          "description": "Object example:",
          "x-mock-file": {
            "/users/alexa": "mocks/alexa.json",           
            "/users/david": "mocks/david.json"
          }
        }
      }
    }
  }
}

Comparing

Compares the earlier-recorded responses to the last responses for endpoints in an OpenAPI 3.x spec file. This command can be used to do integration testing, by treating the recorded responses as snapshots, and comparing those to the latest responses.

Command

Usage: oaat compare [options] <jsonFile> [serverUrl]

Compares recorded responses (referenced by the spec file) to the latest responses

Options:
  -c, --config <file>          Config file to override default config
  -s, --sec-tokens <key=val,>  Pass security token(s) matching the "key" in spec.securitySchemes, with a "value"
  -m --compare-mode <mode>     Comparison mode: "value" (default), "type", "schema"
  -q, --quiet                  no logging
  -v, --verbose                verbose logging
  -h, --help                   display help for command

Validating

This command validates the JSON spec file against the Open API 3.x schema. Any errors are listed in the output.

Command

$ oaat validate --help
Usage: oaat validate [options] <jsonFile>

Validate the API spec file against the OAS 3.x schema

Options:
  -h, --help  display help for command

Config file

oaat has a default configuration which can be overridden using a config file. The config file can be JSON or a CommonJS module which exports an object.

Example:

module.exports = {
  // Shared configuration for all commands
  global: {
    // The number of simultaneous requests HTTP requests to make. Increasing this value
    // can lead to inconsistent results.
    simultaneousRequests: 15,
  },

  // Configuration for the `oaat record` command:
  record: {

    // Path to a subdirectory (relative to the spec file) that contains the response files
    // If you do not wish to put response files into a subdirectory, removeUnsedResponses is
    // automatically set to false to avoid deleting files from your specfile folder!
    responseBasePath: 'responses/',

    // After renaming examples and generating new response files, old response files may no longer
    // be being used. Set this to true to remove the unused (un-referenced by the spec file) response files. 
    removeUnusedResponses: true,

    // A function to generate the name for each mock file.
    // This is the default naming function:
    responseFilenameFn(apiData) {
      // console.log(apiData);  // Print the apiData structure to see what is available
      const exampleName =
        apiData.exampleIndex === 0
          ? `DEFAULT${apiData.exampleName === 'default' ? '' : `_${apiData.exampleName}`}`
          : apiData.exampleName;
      return `${apiData.config.method.toUpperCase()}_${apiData.path.slice(1).replace(/\//g, '_')}-${
        apiData.statusCode
      }_${exampleName}.json`;
    },

    // Whether to update the response file when the new response is an inexact match.
    // For example, if a date field in the response is being ignored (because it always changes)
    // but everything else in the response matches the previous response, should the response
    // file be updated?
    updateResponseWhenInexactMatch: true,

    // Whether to update the response file when the new response's data-types match the
    // old response's data types, but the values are different.
    updateResponseWhenTypesMatch: true,

    // Lint the spec file as well (true)
    andLint: true,
  },

  // Configuration for the `lint` command
  lint: {

    // Sort the spec file's paths alphabetically (true)
    sortPathsAlphabetically: true,

    // Sort the spec file's component.schemas alphabetically (true)
    sortComponentsAlphabetically: true,

    // Updates the parameter and requestBody examples using the x-examples from the 200 response
    // Requires a API server to be in the spec or specified in the command line if any paramaters come from scripts
    // Requires config.securitySchemes (or `sec-tokens` on the command line) to be specified if any APIs specify the "security" property.
    syncExamples: true
  },

  // Configuration for the `build` command
  build: {

    // The path that will serve the Swagger UI
    specUIEndpoint: '/',

    // The path that will serve the original spec itself (not the API Gateway version of the spec)
    specFileEndpoint: '/open-api-spec.json',

    // The value for the <title> element 
    webTitle: 'My Company',
    
    // A logo for the website (shown in the top-left corner)
    webLogoUrl: 'https://www.elitedangerous.com/img/logo-elite-dangerous-icon.c7206b1e.svg',

    // A URI (URL or data:image/x-icon;base64 encode image) for the favicon
    webFaviconHref: 'url or data:image/x-icon;base64',
  },
  // If you wish to record your security tokens here, you may.
  securitySchemes: {
   schemeName1: { value: 'static value' },
   schemeName2: { script: 'path/to/script.js' }
  }
};