@serenity-js/rest
Version:
Serenity/JS Screenplay Pattern library for interacting with REST and other HTTP-based services, supporting comprehensive API testing and blended testing scenarios
443 lines • 18.6 kB
TypeScript
import { URL } from 'node:url';
import { Ability, type SerialisedAbility } from '@serenity-js/core';
import { type AxiosDefaults, type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios';
import type { AxiosRequestConfigDefaults } from '../../io';
/**
* An [ability](https://serenity-js.org/api/core/class/Ability/) that wraps [axios client](https://axios-http.com/docs/api_intro) and enables
* the [actor](https://serenity-js.org/api/core/class/Actor/) to [send](https://serenity-js.org/api/rest/class/Send/)
* [HTTP requests](https://serenity-js.org/api/rest/class/HTTPRequest/) to HTTP APIs.
*
* ## Configuring the ability to call an API
*
* The easiest way to configure the ability to `CallAnApi` is to provide the `baseURL` of your HTTP API,
* and rely on Serenity/JS to offer other sensible defaults:
*
* ```ts
* import { actorCalled } from '@serenity-js/core'
* import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* await actorCalled('Apisitt')
* .whoCan(
* CallAnApi.at('https://api.example.org/')
* )
* .attemptsTo(
* Send.a(GetRequest.to('/v1/users/2')), // GET https://api.example.org/v1/users/2
* Ensure.that(LastResponse.status(), equals(200)),
* )
* ```
*
* Please note that the following Serenity/JS test runner adapters already provide the ability to `CallAnApi` as part of their default configuration,
* so you don't need to configure it yourself:
* - [Playwright Test](https://serenity-js.org/handbook/test-runners/playwright-test/)
* - [WebdriverIO](https://serenity-js.org/handbook/test-runners/webdriverio/)
* - [Protractor](https://serenity-js.org/handbook/test-runners/protractor/)
*
* ### Resolving relative URLs
*
* Serenity/JS resolves request URLs using Node.js [WHATWG URL API](https://nodejs.org/api/url.html#new-urlinput-base).
* This means that the request URL is determined using the resource path resolved in the context of base URL, i.e. `new URL(resourcePath, [baseURL])`.
*
* Consider the following example:
*
* ```ts
* import { actorCalled } from '@serenity-js/core'
* import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* await actorCalled('Apisitt')
* .whoCan(
* CallAnApi.at(baseURL)
* )
* .attemptsTo(
* Send.a(GetRequest.to(resourcePath)),
* Ensure.that(LastResponse.status(), equals(200)),
* )
* ```
*
* In the above example:
* - when `resourcePath` is defined as a full URL, it overrides the base URL
* - when `resourcePath` starts with a forward slash `/`, it replaces any path defined in the base URL
* - when `resourcePath` is not a full URL and doesn't start with a forward slash, it gets appended to the base URL
*
* | baseURL | resourcePath | result |
* | ----------------------------- | -------------------------- | ------------------------------------ |
* | `https://api.example.org/` | `/v1/users/2` | `https://api.example.org/v1/users/2` |
* | `https://example.org/api/v1/` | `users/2` | `https://example.org/api/v1/users/2` |
* | `https://example.org/api/v1/` | `/secure/oauth` | `https://example.org/secure/oauth` |
* | `https://v1.example.org/api/` | `https://v2.example.org/` | `https://v2.example.org/` |
*
* ### Using Axios configuration object
*
* When you need more control over how your Axios instance is configured, provide
* an [Axios configuration object](https://axios-http.com/docs/req_config). For example:
*
* ```ts
* import { actorCalled } from '@serenity-js/core'
* import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* await actorCalled('Apisitt')
* .whoCan(
* CallAnApi.using({
* baseURL: 'https://api.example.org/',
* timeout: 30_000,
* // ... other configuration options
* })
* )
* .attemptsTo(
* Send.a(GetRequest.to('/users/2')),
* Ensure.that(LastResponse.status(), equals(200)),
* )
* ```
*
* ## Working with proxy servers
*
* `CallAnApi` uses an approach described in ["Node.js Axios behind corporate proxies"](https://janmolak.com/node-js-axios-behind-corporate-proxies-8b17a6f31f9d)
* to automatically detect proxy server configuration based on your environment variables.
*
* You can override this default proxy detection mechanism by providing your own proxy configuration object.
*
* ### Automatic proxy support
*
* When the URL you're sending the request to uses the HTTP protocol, Serenity/JS will automatically detect your proxy configuration based on the following environment variables:
* - `npm_config_http_proxy`
* - `http_proxy` and `HTTP_PROXY`
* - `npm_config_proxy`
* - `all_proxy`
*
* Similarly, when the request target URL uses the HTTPS protocol, the following environment variables are used instead:
* - `npm_config_https_proxy`
* - `https_proxy` and `HTTPS_PROXY`
* - `npm_config_proxy`
* - `all_proxy`
*
* Proxy configuration is ignored for both HTTP and HTTPS target URLs matching the proxy bypass rules defined in the following environment variables:
* - `npm_config_no_proxy`
* - `no_proxy` and `NO_PROXY`
*
* ### Custom proxy configuration
*
* To override the automatic proxy detection based on the environment variables, provide a proxy configuration object:
*
* ```ts
* import { actorCalled } from '@serenity-js/core'
* import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* await actorCalled('Apisitt')
* .whoCan(
* CallAnApi.using({
* baseURL: 'https://api.example.org/',
* proxy: {
* protocol: 'http',
* host: 'proxy.example.org',
* port: 9000,
* auth: { // optional
* username: 'proxy-username',
* password: 'proxy-password',
* },
* bypass: 'status.example.org, internal.example.org' // optional
* }
* // ... other configuration options
* })
* )
* .attemptsTo(
* Send.a(GetRequest.to('/users/2')),
* Ensure.that(LastResponse.status(), equals(200)),
* )
* ```
*
* Note that Serenity/JS uses [proxy-agents](https://github.com/TooTallNate/proxy-agents)
* and the approach described in ["Node.js Axios behind corporate proxies"](https://janmolak.com/node-js-axios-behind-corporate-proxies-8b17a6f31f9d)
* to work around [limited proxy support capabilities](https://github.com/axios/axios/issues?q=is%3Aissue+is%3Aopen+proxy) in Axios itself.
*
* ### Bypassing proxy configuration
*
* To bypass the proxy configuration for specific hostnames and IP addresses, you can either:
* - provide the `bypass` property in the proxy configuration object, or
* - use the `no_proxy` environment variable.
*
* The value of the `bypass` property or the `no_proxy` environment variable should be a comma-separated list of hostnames and IP addresses
* that should not be routed through the proxy server, for example: `.com, .serenity-js.org, .domain.com`.
*
* Note that setting the `bypass` property to `example.org` makes the requests to following URLs bypass the proxy server:
* - `api.example.org`
* - `sub.sub.example.org`
* - `example.org`
* - `my-example.org`
*
* :::info
* Serenity/JS doesn't currently support `bypass` rules expressed using CIDR notation, like `192.168.17.0/24`.
* Instead, it uses a simple comma-separated list of hostnames and IP addresses.
* If you need support for CIDR notation, please [raise an issue](https://github.com/serenity-js/serenity-js/issues).
* :::
*
* ### Using Axios instance with proxy support
*
* To have full control over the Axios instance used by the ability to `CallAnApi`, you can create it yourself
* and inject it into the ability.
* This approach allows you to still benefit from automated proxy detection in configuration, while taking advantage
* of the many [Axios plugins](https://www.npmjs.com/search?q=axios).
*
* ```ts
* import { actorCalled } from '@serenity-js/core'
* import { createAxios, CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* import axiosRetry from 'axios-retry'
*
* const instance = createAxios({ baseURL: 'https://api.example.org/' })
* axiosRetry(instance, { retries: 3 })
*
* await actorCalled('Apisitt')
* .whoCan(
* CallAnApi.using(instance)
* )
* .attemptsTo(
* Send.a(GetRequest.to('/users/2')),
* Ensure.that(LastResponse.status(), equals(200)),
* )
* ```
*
* ### Using raw Axios instance
*
* If you don't want Serenity/JS to enhance your Axios instance with proxy support, instantiate the ability to
* `CallAnApi` using its constructor directly.
* Note, however, that by using this approach you're taking the responsibility for all the aspects of configuring Axios.
*
* ```ts
* import { actorCalled } from '@serenity-js/core'
* import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* import { axiosCreate } from '@serenity-js/rest'
* import axiosRetry from 'axios-retry'
*
* const instance = axiosCreate({ baseURL: 'https://api.example.org/' })
* axiosRetry(instance, { retries: 3 })
*
* await actorCalled('Apisitt')
* .whoCan(
* new CallAnApi(instance) // using the constructor ensures your axios instance is not modified in any way.
* )
* .attemptsTo(
* // ...
* )
* ```
*
* ### Serenity/JS defaults
*
* When using [`CallAnApi.at`](https://serenity-js.org/api/rest/class/CallAnApi/#at) or [`CallAnApi.using`](https://serenity-js.org/api/rest/class/CallAnApi/#using) with a configuration object, Serenity/JS
* merges your [Axios request configuration](https://axios-http.com/docs/req_config) with the following defaults:
* - `timeout`: 10 seconds
*
*
* You can override them by specifying the given property in your configuration object, for example:
* ```ts
* import { actorCalled } from '@serenity-js/core'
* import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* await actorCalled('Apisitt')
* .whoCan(
* CallAnApi.using({
* baseURL: 'https://api.example.org/',
* timeout: 30_000
* })
* )
* .attemptsTo(
* Send.a(GetRequest.to('/users/2')),
* Ensure.that(LastResponse.status(), equals(200)),
* )
* ```
*
* ## Interacting with multiple APIs
*
* Some test scenarios might require you to interact with multiple HTTP APIs. With Serenity/JS you can do this
* using either API-specific actors, or by specifying full URLs when performing the requests.
*
* The following examples will assume that the test scenarios needs to interact with the following APIs:
* - `https://testdata.example.org/api/v1/`
* - `https://shop.example.org/api/v1/`
*
* Let's also assume that the `testdata` API allows the automation to manage the test data used by the `shop` API.
*
* ### Using API-specific actors
*
* To create API-specific actors, configure your [test runner](https://serenity-js.org/handbook/test-runners/) with a [cast](https://serenity-js.org/api/core/class/Cast/)
* that gives your actors appropriate abilities based, for example, on their name:
*
* ```ts
* import { beforeEach } from 'mocha'
* import { Actor, Cast, engage } from '@serenity-js/core'
* import { CallAnApi } from '@serenity-js/rest'
*
* export class MyActors implements Cast {
* prepare(actor: Actor): Actor {
* switch(actor.name) {
* case 'Ted':
* return actor.whoCan(CallAnApi.at('https://testdata.example.org/api/v1/'))
* case 'Shelly':
* return actor.whoCan(CallAnApi.at('https://shop.example.org/api/v1/'))
* default:
* return actor;
* }
* }
* }
*
* beforeEach(() => engage(new MyActors()))
* ```
*
* Next, retrieve the appropriate actor in your test scenario using [`actorCalled`](https://serenity-js.org/api/core/function/actorCalled/), for example:
*
* ```ts
* import { describe, it, beforeEach } from 'mocha'
* import { actorCalled, engage } from '@serenity-js/core
* import { Send, GetRequest, PostRequest, LastResponse } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* describe('Multi-actor API testing', () => {
* beforeEach(() => engage(new MyActors()))
*
* it('allows each actor to interact with their API', async () => {
*
* await actorCalled('Ted').attemptsTo(
* Send.a(PostRequest.to('products').with({ name: 'Apples', price: '£2.50' })),
* Ensure.that(LastResponse.status(), equals(201)),
* )
*
* await actorCalled('Shelly').attemptsTo(
* Send.a(GetRequest.to('?product=Apples')),
* Ensure.that(LastResponse.status(), equals(200)),
* Ensure.that(LastResponse.body(), equals([
* { name: 'Apples', price: '£2.50' }
* ])),
* )
* })
* })
* ```
*
* ### Using full URLs
*
* If you prefer to have a single actor interacting with multiple APIs, you can specify the full URL for every request:
*
* ```ts
* import { describe, it, beforeEach } from 'mocha'
* import { actorCalled, Cast, engage } from '@serenity-js/core
* import { CallAnApi, Send, GetRequest, PostRequest, LastResponse } from '@serenity-js/rest'
* import { Ensure, equals } from '@serenity-js/assertions'
*
* describe('Multi-actor API testing', () => {
* beforeEach(() => engage(
* Cast.where(actor => actor.whoCan(CallAnApi.using({})))
* ))
*
* it('allows each actor to interact with their API', async () => {
*
* await actorCalled('Alice').attemptsTo(
* Send.a(PostRequest.to('https://testdata.example.org/api/v1/products')
* .with({ name: 'Apples', price: '£2.50' })),
* Ensure.that(LastResponse.status(), equals(201)),
*
* Send.a(GetRequest.to('https://shop.example.org/api/v1/?product=Apples')),
* Ensure.that(LastResponse.status(), equals(200)),
* Ensure.that(LastResponse.body(), equals([
* { name: 'Apples', price: '£2.50' }
* ])),
* )
* })
* })
* ```
*
* ## Learn more
* - [Axios: Configuring requests](https://axios-http.com/docs/req_config)
* - [MDN: HTTP methods documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
*
* @group Abilities
*/
export declare class CallAnApi extends Ability {
private readonly axiosInstance;
private lastResponse;
/**
* Produces an [ability](https://serenity-js.org/api/core/class/Ability/) to call a REST API at a specified `baseURL`;
*
* This is the same as invoking `CallAnApi.using({ baseURL: 'https://example.org' })`
*
* @param baseURL
*/
static at(baseURL: URL | string): CallAnApi;
/**
* Produces an [ability](https://serenity-js.org/api/core/class/Ability/) to call an HTTP API using the given Axios instance,
* or an Axios request configuration object.
*
* When you provide an [Axios configuration object](https://axios-http.com/docs/req_config),
* it gets shallow-merged with the following defaults:
* - request timeout of 10 seconds
* - automatic proxy support based on
* your [environment variables](https://www.npmjs.com/package/proxy-from-env#environment-variables)
*
* When you provide an Axios instance, it's enhanced with proxy support and no other modifications are made.
*
* If you don't want Serenity/JS to augment or modify your Axios instance in any way,
* please use the [`CallAnApi.constructor`](https://serenity-js.org/api/rest/class/CallAnApi/#constructor) directly.
*
* @param axiosInstanceOrConfig
*/
static using(axiosInstanceOrConfig: AxiosInstance | AxiosRequestConfigDefaults): CallAnApi;
/**
* #### Learn more
* - [AxiosInstance](https://axios-http.com/docs/instance)
*
* @param axiosInstance
* A pre-configured instance of the Axios HTTP client
*/
constructor(axiosInstance: AxiosInstance);
/**
* Allows for the original Axios config to be changed after
* the [ability](https://serenity-js.org/api/core/class/Ability/) to [`CallAnApi`](https://serenity-js.org/api/rest/class/CallAnApi/)
* has been instantiated and given to the [`Actor`](https://serenity-js.org/api/core/class/Actor/).
*
* #### Learn more
* - [AxiosRequestConfig](https://axios-http.com/docs/req_config)
*
* @param fn
*/
modifyConfig(fn: (original: AxiosDefaults<any>) => any): void;
/**
* Sends an HTTP request to a specified url.
* Response will be cached and available via [`CallAnApi.mapLastResponse`](https://serenity-js.org/api/rest/class/CallAnApi/#mapLastResponse).
*
* #### Learn more
* - [AxiosRequestConfig](https://axios-http.com/docs/req_config)
* - [AxiosResponse](https://axios-http.com/docs/res_schema)
*
* @param config
* Axios request configuration, which can be used to override the defaults
* provided when the [ability](https://serenity-js.org/api/core/class/Ability/)
* to [`CallAnApi`](https://serenity-js.org/api/rest/class/CallAnApi/) was instantiated.
*/
request(config: AxiosRequestConfig): Promise<AxiosResponse>;
/**
* Resolves the final URL, based on the [`AxiosRequestConfig`](https://axios-http.com/docs/req_config) provided
* and any defaults that the [`AxiosInstance`](https://axios-http.com/docs/instance) has been configured with.
*
* Note that unlike Axios, this method uses the Node.js [WHATWG URL API](https://nodejs.org/api/url.html#new-urlinput-base)
* to ensure URLs are correctly resolved.
*
* @param config
*/
resolveUrl(config: AxiosRequestConfig): string;
/**
* Maps the last cached response to another type.
* Useful when you need to extract a portion of the [`AxiosResponse`](https://axios-http.com/docs/res_schema) object.
*
* #### Learn more
* - [AxiosResponse](https://axios-http.com/docs/res_schema)
*
* @param mappingFunction
*/
mapLastResponse<T>(mappingFunction: (response: AxiosResponse) => T): T;
toJSON(): SerialisedAbility;
}
//# sourceMappingURL=CallAnApi.d.ts.map