nginx-testing

Support for integration/acceptance testing of nginx configuration.

Usage no npm install needed!

<script type="module">
  import nginxTesting from 'https://cdn.skypack.dev/nginx-testing';
</script>

README

= nginx testing :toc: macro :toc-title: // custom :npm-name: nginx-testing :gh-name: jirutka/{npm-name}

ifdef::env-github[] image:https://github.com/{gh-name}/workflows/CI/badge.svg[Build Status, link=https://github.com/{gh-name}/actions?query=workflow%3A%22CI%22] image:https://img.shields.io/npm/v/{npm-name}.svg[npm Version, link="https://www.npmjs.org/package/{npm-name}"] image:https://badgen.net/bundlephobia/dependency-count/{npm-name}[Dependency Count, link="https://bundlephobia.com/result?p={npm-name}"] endif::env-github[]

This project provides support for easy integration/acceptance testing of https://nginx.org/[nginx] configuration using TypeScript or JavaScript, primarily for testing https://nginx.org/en/docs/njs/[njs] (NGINX JavaScript) scripts.

It allows you to run tests against various nginx versions on any Linux, macOS or Windows (x64) system without need to install nginx or use some overcomplex methods such as Docker footnote:[Yes, that’s right, you don’t need Docker to run a damn binary!]. {npm-name} automatically downloads a precompiled nginx binary for your system and architecture from project https://github.com/jirutka/nginx-binaries[nginx-binaries].

ifndef::npm-readme[] [discrete] == Table of Contents

toc::[] endif::npm-readme[]

== Installation

[source, sh, subs="+attributes"]

using npm:

npm install --save-dev {npm-name}

or using yarn:

yarn add --dev {npm-name}

== Usage Examples

=== Testing with Mocha

.example.test.ts: [source, ts]

import { strict as assert } from 'assert' import { after, afterEach, before, beforeEach, test } from 'mocha' import { startNginx, NginxServer } from 'nginx-testing' import fetch from 'node-fetch'

let nginx: NginxServer

before(async () => { nginx = await startNginx({ version: '1.18.x', configPath: './nginx.conf' }) })

after(async () => { await nginx.stop() })

beforeEach(async () => { // Consume logs (i.e. clean them before the test). await nginx.readAccessLog() await nginx.readErrorLog() })

afterEach(async function () { // Print logs if the test failed. if (this.currentTest?.state === 'failed') { console.error('Access log:\n' + await nginx.readAccessLog()) console.error('Error log:\n' + await nginx.readErrorLog()) } })

test('GET / results in HTTP 200', async () => { const resp = await fetch(http://localhost:${nginx.port}/) assert.equal(resp.status, 200) })

.nginx.conf: [source, nginx]

events { } http { server { listen localhost:PORT;

location / {
  return 200 "OK";
}

} }

== API :Writable: link:https://nodejs.org/api/stream.html#stream_class_stream_writable[stream.Writable]

// NOTE: Keep the API section in sync with TSDoc comments in the sources (until I figure out how to generate it).

// Pandoc conversion to Markdown doesn't handle definition lists. ifdef::npm-readme[] https://github.com/{gh-name}#api[See on GitHub].

endif::npm-readme[] ifndef::npm-readme[]

[[startNginx]] startNginx (opts: <<NginxOptions>>) => Promise<<<NginxServer>>>:: Starts nginx server with the given configuration. + .Example: [source, ts]

import { startNginx, NginxServer } from 'nginx-testing' import fetch from 'node-fetch'

let nginx: NginxServer

before(async () => { nginx = await startNginx({ version: '1.18.x', configPath: './nginx.conf' }) }) after(nginx.stop)

test('GET / results in HTTP 200', async () => { const resp = await fetch(http://localhost:${nginx.port}/) assert(resp.status === 200) })

[[nginxVersionInfo]] nginxVersionInfo (nginxBinPath: string) => Promise<<<NginxVersionInfo>>>:: Executes the nginx binary nginxBinPath with option -V and returns parsed version and info about the modules it was compiled with(out).

[[parseConf]] parseConf (source: string) => <<NginxConfEditor>>:: Parses the given nginx config.

[[setLogger]] setLogger (logger: object) => void:: Use the given logger -- an object with functions debug, info, warn, and error. Undefined logging functions will be replaced with no-op. + See section <> for more information.

=== NginxOptions

Options for <<startNginx, startNginx()>>.

binPath (?string):: Name or path of the nginx binary to start. Defaults to 'nginx'. + This option is ignored if version is provided.

version (?string):: A SemVer version range specifying the nginx version to run. + Nginx binary for your OS and architecture will be downloaded from https://github.com/jirutka/nginx-binaries[nginx-binaries]. It will be stored in directory .cache/nginx-binaries/ inside the nearest writeable node_modules directory or in nginx-binaries/ inside the system-preferred temp directory. + Not all versions are available. You can find a list of available binaries at https://jirutka.github.io/nginx-binaries/[nginx-binaries].

config (?string):: +

Nginx configuration to use.

If configPath is provided, the processed config will be written to a temporary file .<filename>~ (where <filename> is a filename from configPath) in the configPath’s directory (e.g. conf/nginx.conf -> conf/.nginx.conf~). Otherwise it will be written into nginx.conf file in workDir. In either case, this file will be automatically deleted after stopping the nginx.

The config may include the following placeholders which will be replaced with corresponding values:

  • ++__ADDRESS__++ -- The address as specified in bindAddress.
  • ++__CONFDIR__++ -- Path to directory with the config file as specified in configPath.
  • ++__CWD__++ -- The current working directory as reported by process.cwd().
  • ++__WORKDIR__++ -- Path to the nginx’s working directory as specified in workDir.
  • ++__PORT__++, ++__PORT_1__++, ..., ++__PORT_9__++ -- The port numbers as specified in ports and preferredPorts.

It will be modified for compatibility with the runner by applying patch operations specified in configPatch variable.

Either configPath, or config must be provided!

configPath (?string):: Path of the nginx configuration file to use. + This file will be processed and the resulting config file will be written to a temporary file .<filename>~ (where <filename> is a filename from configPath) in the configPath’s directory (e.g. conf/nginx.conf -> conf/.nginx.conf~). This temporary file will be automatically deleted after stopping the nginx. + See config option for information about placeholders and patching. + Either configPath, or config must be provided!

bindAddress (?string):: Hostname or IP address the port(s) will be binding on. This is used when searching for free ports (see preferredPorts) and for substituting ++__ADDRESS__++ placeholder in the given nginx config. Defaults to '127.0.0.1'.

ports (?number[]):: A list of port numbers for substituting ++__PORT__++, ++__PORT_1__++, ..., ++__PORT_9__++ placeholders in the given nginx config. Unlike preferredPorts, these are not checked for availability and nginx will fail to start if any of the provided and used ports is unavailable. + If it’s not provided or more ports are needed, next ports are selected from the preferredPorts or randomly.

preferredPorts (?number[]):: A list of preferred port numbers to use for substituting ++__PORT__++, ++__PORT_1__++, ..., ++__PORT_9__++ placeholders in the given nginx config. + Unavailable ports (used by some other program or restricted by OS) are skipped. If there are no preferred ports left and another port is needed, a random port number is used.

workDir (?string):: Path of a directory that will be passed as a prefix (-p) into nginx. It will be automatically created if doesn’t exist. + If not provided, an unique temporary directory will be created: .cache/nginx-testing-XXXXXX/ relative to the nearest writable node_modules (nearest to process.cwd()) or nginx-testing-XXXXXX/ in the system-preferred temp directory. The created directory will be automatically deleted after stopping.

errorLog (?string | ?{Writable}):: +

One of:

  • 'buffer' -- Collect the nginx’s stderr to a buffer that can be read using readErrorLog() (default).
  • 'ignore' -- Ignore nginx’s stderr.
  • 'inherit' -- Pass through the nginx’s stderr output to the Node process.
  • <{Writable}> -- A writable stream to pipe the nginx’s stderr to.

Nginx error log is expected to be redirected to stderr. Directive error_log stderr info; will be automatically added to the config, unless there’s already error_log defined in the main context.

accessLog (?string | ?{Writable}):: +

One of:

  • 'buffer' -- Collect the nginx’s access log to a buffer that can be read using readAccessLog() (default).
  • 'ignore' -- Ignore nginx’s access log.
  • <{Writable}> -- A writable stream to pipe the nginx’s access log to.

Nginx access log is expected to be redirected to file <workDir>/access.log. Directive access_log access.log; will be automatically added to the config, unless there’s already access_log defined in the http context.

startTimeoutMsec (?number):: Number of milliseconds after the start to wait for the nginx to respond to the health-check request (HEAD ++http://<bindAddress>:<ports[0]>/health++). Any HTTP status is considered as success -- it just checks if the nginx is listening and responding. + Defaults to 1000.

=== RestartOptions

config (?string):: The same as in <>.

configPath (?string):: The same as in <>.

=== NginxServer

A return value of <<startNginx, startNginx()>>.

config (string):: The current nginx configuration.

pid (number):: PID of the nginx process.

port (number):: Number of the first port allocated for nginx, i.e. the port on which nginx should listen for connections. It’s the same as ports[0].

ports (number[]):: A list of port numbers allocated for nginx.

workDir (string):: Path of the nginx’s working directory.

readAccessLog () => Promise<string>:: Reads new messages from the access log since the last call of readAccessLog(). + Throws Error if the process was created with option accessLog other than 'buffer' or undefined.

readErrorLog () => Promise<string>:: Reads new messages from the error log since the last call of readErrorLog(). + Throws Error if the process was created with option errorLog other than 'buffer' or undefined.

reload (?<<RestartOptions>>) => Promise<void>:: Reloads the nginx (using SIGHUP), optionally with a new configuration. Options config and configPath are mutually exclusive here. + Nginx can be reloaded only when running with the master process. This is disabled by default, but you can override it by declaring master_process on in the config. + Important: The function you are looking for is restart(). Use reload() only if you know that you cannot use restart(). + Caution: This function doesn’t work on Windows! + Throws Error if nginx was started with master_process off or if running on Windows (win32 platform).

restart (?<<RestartOptions>>) => Promise<void>:: Restarts the nginx, optionally with a new configuration. Options config and configPath are mutually exclusive here. The new nginx process will be started with the same ports, working directory etc.

stop () => Promise<void>:: Stops the nginx and cleans-up temporary files and directories.

=== NginxVersionInfo

Parsed output of nginx -V returned by <<nginxVersionInfo, nginxVersionInfo()>>.

version (string):: Nginx version number (e.g. '1.18.0').

modules (Object.<string, string>):: An object of module names as properties with value 'with', 'with-dynamic', or 'without'. + .Example: [source, ts]

{ http_fastcgi: 'without', http_geoip: 'with-dynamic', http_ssl: 'with', }

=== NginxConfEditor

Nginx configuration editor returned by <<parseConf, parseConf()>>.

get (path: string) => string | string[] | undefined:: Returns a value of a directive at the path specified by a JSON Pointer (e.g. /http/servers/0/listen). +

  • If the directive is not declared, returns undefined.
  • If the path points to an unnamed block (e.g. server), returns an empty string.
  • If an intermediate directive is declared multiple times and no index is specified in the path (e.g. /http/servers/listen), the first one is selected (/http/servers/0/listen).
  • If the path points to a directive that is declared multiple times (in the same context), returns an array of each declaration’s value.

applyPatch (patch: <<PatchOperation>>[]) => this:: Applies the specified patch operations on the config. + Throws RangeError if some intermediate directive on the path does not exist.

toString () => string:: Dumps the config back to string.

=== PatchOperation

A patch operation to be performed on nginx config.

It’s an object with the following properties:

op (string):: The operation name; one of:

  • 'add' -- Adds a directive.
  • 'default' -- Sets a directive if it’s not declared yet.
  • 'remove' -- Removes a directive.
  • 'set' -- Sets a directive and removes its existing declarations in the same context.

path (string):: A JSON Pointer of the directive to be added, set or removed. + For example, /http/server/1/listen points to a directive listen in the second server context inside http context. See documentation of get function in <> for more information.

value (string):: A value of the directive (not defined for op 'remove').

This is based on http://jsonpatch.com/[JSON Patch], but with a different operations.

=== Logging

. If https://github.com/Download/anylogger[anylogger] is available and initialized (any adapter has been registered), then: ** all log messages will go through anylogger logger nginx-binaries.

. If https://www.npmjs.com/package/debug[debug] is available, then: ** debug messages will be logged via debug logger nginx-binaries, others (error, warn, info) via console.

. otherwise: ** error, warn, and info messages will be logged via https://nodejs.org/api/console.html[console], debug messages will be discarded.

If none of these options is suitable, you can provide your own logger using <<setLogger, setLogger()>>:

[source, js, subs="+attributes"]

import { setLogger } from '{npm-name}'

setLogger({ warn: console.warn, error: console.error, // undefined logging functions will be replaced with no-op })

endif::npm-readme[]

== CLI

// Pandoc conversion to Markdown doesn't handle definition lists. ifdef::npm-readme[] https://github.com/{gh-name}#cli[See on GitHub].

endif::npm-readme[] ifndef::npm-readme[]

=== start-nginx

// NOTE: Keep this section in sync with --help message in nginxRunnerCli.ts (until I write a script to generate it).


start-nginx [options] start-nginx -h | --help

Start nginx server with the given config and reload it on changes.

==== Arguments

:: Path of the nginx configuration file.

==== Options

-b --bin-path :: Name or path of the nginx binary to start. Defaults to nginx. This option is ignored if --version is specified.

-v --version :: A SemVer version range specifying the nginx version to download from https://github.com/jirutka/nginx-binaries[nginx-binaries] a and run.

-A --bind-address :: Hostname or IP address to bind the port(s) on. Defaults to 127.0.0.1.

-p --port :: Port number(s) for substituting ++__PORT__++, ++__PORT_1__++, ..., ++__PORT_9__++ placeholders in the nginx config. Repeat this option for more ports. Defaults to random port numbers.

-d --work-dir

:: Path of a directory that will be passed as a prefix into nginx. If not provided, a temporary directory will be automatically created.

-T --start-timeout :: Number of milliseconds after the start to wait for the nginx to respond to the health-check request. Defaults to 1,000 ms.

-w --watch :: Watch file or directory (recursively) and reload nginx on changes. is watched implicitly. Repeat this option for more paths.

-D --watch-delay :: Delay time between reloads in milliseconds. Defaults to 200 ms.

-h --help:: Show help message and exit.

endif::npm-readme[]

== License

This project is licensed under http://opensource.org/licenses/MIT/[MIT License].