@serenity-js/rest 3.10.4 → 3.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/lib/index.d.ts +0 -1
  3. package/lib/index.d.ts.map +1 -1
  4. package/lib/index.js +0 -1
  5. package/lib/index.js.map +1 -1
  6. package/lib/screenplay/abilities/AxiosRequestConfigDefaults.d.ts +14 -0
  7. package/lib/screenplay/abilities/AxiosRequestConfigDefaults.d.ts.map +1 -0
  8. package/lib/screenplay/abilities/AxiosRequestConfigDefaults.js +3 -0
  9. package/lib/screenplay/abilities/AxiosRequestConfigDefaults.js.map +1 -0
  10. package/lib/screenplay/abilities/CallAnApi.d.ts +297 -38
  11. package/lib/screenplay/abilities/CallAnApi.d.ts.map +1 -1
  12. package/lib/screenplay/abilities/CallAnApi.js +353 -44
  13. package/lib/screenplay/abilities/CallAnApi.js.map +1 -1
  14. package/lib/screenplay/abilities/index.d.ts +1 -0
  15. package/lib/screenplay/abilities/index.d.ts.map +1 -1
  16. package/lib/screenplay/abilities/index.js +1 -0
  17. package/lib/screenplay/abilities/index.js.map +1 -1
  18. package/lib/screenplay/abilities/proxy/ProxyAgent.d.ts +55 -0
  19. package/lib/screenplay/abilities/proxy/ProxyAgent.d.ts.map +1 -0
  20. package/lib/screenplay/abilities/proxy/ProxyAgent.js +107 -0
  21. package/lib/screenplay/abilities/proxy/ProxyAgent.js.map +1 -0
  22. package/lib/screenplay/abilities/proxy/axiosProxyOverridesFor.d.ts +12 -0
  23. package/lib/screenplay/abilities/proxy/axiosProxyOverridesFor.d.ts.map +1 -0
  24. package/lib/screenplay/abilities/proxy/axiosProxyOverridesFor.js +35 -0
  25. package/lib/screenplay/abilities/proxy/axiosProxyOverridesFor.js.map +1 -0
  26. package/lib/screenplay/abilities/proxy/createUrl.d.ts +10 -0
  27. package/lib/screenplay/abilities/proxy/createUrl.d.ts.map +1 -0
  28. package/lib/screenplay/abilities/proxy/createUrl.js +30 -0
  29. package/lib/screenplay/abilities/proxy/createUrl.js.map +1 -0
  30. package/lib/screenplay/abilities/proxy/index.d.ts +2 -0
  31. package/lib/screenplay/abilities/proxy/index.d.ts.map +1 -0
  32. package/lib/screenplay/abilities/proxy/index.js +18 -0
  33. package/lib/screenplay/abilities/proxy/index.js.map +1 -0
  34. package/lib/screenplay/index.d.ts +1 -0
  35. package/lib/screenplay/index.d.ts.map +1 -1
  36. package/lib/screenplay/index.js +1 -0
  37. package/lib/screenplay/index.js.map +1 -1
  38. package/lib/screenplay/interactions/Send.d.ts +3 -3
  39. package/lib/screenplay/interactions/Send.js +3 -3
  40. package/lib/{models → screenplay/models}/DeleteRequest.d.ts +1 -1
  41. package/lib/screenplay/models/DeleteRequest.d.ts.map +1 -0
  42. package/lib/{models → screenplay/models}/DeleteRequest.js +1 -1
  43. package/lib/screenplay/models/DeleteRequest.js.map +1 -0
  44. package/lib/{models → screenplay/models}/GetRequest.d.ts +1 -1
  45. package/lib/screenplay/models/GetRequest.d.ts.map +1 -0
  46. package/lib/{models → screenplay/models}/GetRequest.js +1 -1
  47. package/lib/screenplay/models/GetRequest.js.map +1 -0
  48. package/lib/screenplay/models/HTTPRequest.d.ts.map +1 -0
  49. package/lib/screenplay/models/HTTPRequest.js.map +1 -0
  50. package/lib/{models → screenplay/models}/HeadRequest.d.ts +1 -1
  51. package/lib/screenplay/models/HeadRequest.d.ts.map +1 -0
  52. package/lib/{models → screenplay/models}/HeadRequest.js +1 -1
  53. package/lib/screenplay/models/HeadRequest.js.map +1 -0
  54. package/lib/{models → screenplay/models}/OptionsRequest.d.ts +1 -1
  55. package/lib/screenplay/models/OptionsRequest.d.ts.map +1 -0
  56. package/lib/{models → screenplay/models}/OptionsRequest.js +1 -1
  57. package/lib/screenplay/models/OptionsRequest.js.map +1 -0
  58. package/lib/{models → screenplay/models}/PatchRequest.d.ts +1 -1
  59. package/lib/screenplay/models/PatchRequest.d.ts.map +1 -0
  60. package/lib/{models → screenplay/models}/PatchRequest.js +1 -1
  61. package/lib/screenplay/models/PatchRequest.js.map +1 -0
  62. package/lib/{models → screenplay/models}/PostRequest.d.ts +1 -1
  63. package/lib/screenplay/models/PostRequest.d.ts.map +1 -0
  64. package/lib/{models → screenplay/models}/PostRequest.js +1 -1
  65. package/lib/screenplay/models/PostRequest.js.map +1 -0
  66. package/lib/{models → screenplay/models}/PutRequest.d.ts +2 -2
  67. package/lib/screenplay/models/PutRequest.d.ts.map +1 -0
  68. package/lib/{models → screenplay/models}/PutRequest.js +2 -2
  69. package/lib/screenplay/models/PutRequest.js.map +1 -0
  70. package/lib/screenplay/models/index.d.ts.map +1 -0
  71. package/lib/screenplay/models/index.js.map +1 -0
  72. package/lib/screenplay/questions/LastResponse.d.ts +2 -2
  73. package/lib/screenplay/questions/LastResponse.js +2 -2
  74. package/package.json +11 -6
  75. package/src/index.ts +0 -1
  76. package/src/screenplay/abilities/AxiosRequestConfigDefaults.ts +15 -0
  77. package/src/screenplay/abilities/CallAnApi.ts +355 -48
  78. package/src/screenplay/abilities/index.ts +1 -0
  79. package/src/screenplay/abilities/proxy/ProxyAgent.ts +129 -0
  80. package/src/screenplay/abilities/proxy/axiosProxyOverridesFor.ts +39 -0
  81. package/src/screenplay/abilities/proxy/createUrl.ts +39 -0
  82. package/src/screenplay/abilities/proxy/index.ts +1 -0
  83. package/src/screenplay/index.ts +1 -0
  84. package/src/screenplay/interactions/Send.ts +3 -3
  85. package/src/{models → screenplay/models}/DeleteRequest.ts +1 -1
  86. package/src/{models → screenplay/models}/GetRequest.ts +1 -1
  87. package/src/{models → screenplay/models}/HeadRequest.ts +1 -1
  88. package/src/{models → screenplay/models}/OptionsRequest.ts +1 -1
  89. package/src/{models → screenplay/models}/PatchRequest.ts +1 -1
  90. package/src/{models → screenplay/models}/PostRequest.ts +1 -1
  91. package/src/{models → screenplay/models}/PutRequest.ts +2 -2
  92. package/src/screenplay/questions/LastResponse.ts +2 -2
  93. package/lib/models/DeleteRequest.d.ts.map +0 -1
  94. package/lib/models/DeleteRequest.js.map +0 -1
  95. package/lib/models/GetRequest.d.ts.map +0 -1
  96. package/lib/models/GetRequest.js.map +0 -1
  97. package/lib/models/HTTPRequest.d.ts.map +0 -1
  98. package/lib/models/HTTPRequest.js.map +0 -1
  99. package/lib/models/HeadRequest.d.ts.map +0 -1
  100. package/lib/models/HeadRequest.js.map +0 -1
  101. package/lib/models/OptionsRequest.d.ts.map +0 -1
  102. package/lib/models/OptionsRequest.js.map +0 -1
  103. package/lib/models/PatchRequest.d.ts.map +0 -1
  104. package/lib/models/PatchRequest.js.map +0 -1
  105. package/lib/models/PostRequest.d.ts.map +0 -1
  106. package/lib/models/PostRequest.js.map +0 -1
  107. package/lib/models/PutRequest.d.ts.map +0 -1
  108. package/lib/models/PutRequest.js.map +0 -1
  109. package/lib/models/index.d.ts.map +0 -1
  110. package/lib/models/index.js.map +0 -1
  111. /package/lib/{models → screenplay/models}/HTTPRequest.d.ts +0 -0
  112. /package/lib/{models → screenplay/models}/HTTPRequest.js +0 -0
  113. /package/lib/{models → screenplay/models}/index.d.ts +0 -0
  114. /package/lib/{models → screenplay/models}/index.js +0 -0
  115. /package/src/{models → screenplay/models}/HTTPRequest.ts +0 -0
  116. /package/src/{models → screenplay/models}/index.ts +0 -0
@@ -19,7 +19,7 @@ const abilities_1 = require("../abilities");
19
19
  * author: string;
20
20
  * }
21
21
  *
22
- * await actorCalled('Apisit')
22
+ * await actorCalled('Apisitt')
23
23
  * .whoCan(CallAnApi.at('https://api.example.org/'))
24
24
  * .attemptsTo(
25
25
  * Send.a(GetRequest.to('/books/0-688-00230-7')),
@@ -61,7 +61,7 @@ const abilities_1 = require("../abilities");
61
61
  * ```
62
62
  *
63
63
  * ## Learn more
64
- * - [AxiosResponse](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L133-L140)
64
+ * - [AxiosResponse](https://axios-http.com/docs/res_schema)
65
65
  *
66
66
  * @group Questions
67
67
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serenity-js/rest",
3
- "version": "3.10.4",
3
+ "version": "3.11.1",
4
4
  "description": "Test REST APIs with Serenity/JS",
5
5
  "author": {
6
6
  "name": "Jan Molak",
@@ -45,14 +45,19 @@
45
45
  "node": "^16.13 || ^18.12 || ^20"
46
46
  },
47
47
  "dependencies": {
48
- "@serenity-js/core": "3.10.4",
49
- "axios": "^1.5.0"
48
+ "@serenity-js/core": "3.11.1",
49
+ "agent-base": "^7.1.0",
50
+ "axios": "^1.5.1",
51
+ "http-proxy-agent": "^7.0.0",
52
+ "https-proxy-agent": "^7.0.2",
53
+ "lru-cache": "^10.0.1",
54
+ "proxy-from-env": "^1.1.0"
50
55
  },
51
56
  "devDependencies": {
52
57
  "@integration/testing-tools": "3.0.0",
53
- "@serenity-js/assertions": "3.10.4",
58
+ "@serenity-js/assertions": "3.11.1",
54
59
  "@types/chai": "^4.3.6",
55
- "@types/mocha": "^10.0.1",
60
+ "@types/mocha": "^10.0.2",
56
61
  "axios-mock-adapter": "1.22.0",
57
62
  "c8": "8.0.1",
58
63
  "mocha": "^10.2.0",
@@ -60,5 +65,5 @@
60
65
  "ts-node": "^10.9.1",
61
66
  "typescript": "5.1.6"
62
67
  },
63
- "gitHead": "28f12bd6029a9a6c1d8e492486138bf0c83916cd"
68
+ "gitHead": "b3e36f6e42eb2c545afa961946469fb6dd93f4df"
64
69
  }
package/src/index.ts CHANGED
@@ -1,2 +1 @@
1
- export * from './models';
2
1
  export * from './screenplay';
@@ -0,0 +1,15 @@
1
+ import { type CreateAxiosDefaults } from 'axios';
2
+
3
+ export type AxiosRequestConfigProxyDefaults = {
4
+ host: string;
5
+ port?: number; // SOCKS proxies don't require port number
6
+ auth?: {
7
+ username: string;
8
+ password: string;
9
+ };
10
+ protocol?: string;
11
+ }
12
+
13
+ export type AxiosRequestConfigDefaults<Data = any> = Omit<CreateAxiosDefaults<Data>, 'proxy'> & {
14
+ proxy?: AxiosRequestConfigProxyDefaults | false;
15
+ }
@@ -1,13 +1,31 @@
1
- import { Ability, ConfigurationError, LogicError, TestCompromisedError } from '@serenity-js/core';
2
- import type { AxiosDefaults, AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
3
- import axios from 'axios';
1
+ import { Ability, ConfigurationError, Duration, LogicError, TestCompromisedError } from '@serenity-js/core';
2
+ import axios, {
3
+ Axios,
4
+ type AxiosDefaults,
5
+ type AxiosError,
6
+ type AxiosInstance,
7
+ type AxiosRequestConfig,
8
+ type AxiosResponse,
9
+ type CreateAxiosDefaults,
10
+ } from 'axios';
11
+
12
+ import type { AxiosRequestConfigDefaults, AxiosRequestConfigProxyDefaults } from './AxiosRequestConfigDefaults';
13
+ import { axiosProxyOverridesFor } from './proxy';
4
14
 
5
15
  /**
6
- * An {@apilink Ability} that enables the {@apilink Actor} to call an HTTP API.
16
+ * An {@apilink Ability} that wraps [axios client](https://axios-http.com/docs/api_intro) and enables
17
+ * the {@apilink Actor} to {@apilink Send} {@apilink HTTPRequest|HTTP requests} to HTTP APIs.
18
+ *
19
+ * `CallAnApi` uses [`proxy-from-env`](https://www.npmjs.com/package/proxy-from-env) and an approach
20
+ * described in ["Node.js Axios behind corporate proxies"](https://janmolak.com/node-js-axios-behind-corporate-proxies-8b17a6f31f9d)
21
+ * to automatically detect proxy server configuration based
22
+ * on your [environment variables](https://www.npmjs.com/package/proxy-from-env#environment-variables).
23
+ * You can override this configuration if needed.
7
24
  *
8
- * If you need to connect via a proxy, check out ["Using Axios behind corporate proxies"](https://janmolak.com/node-js-axios-behind-corporate-proxies-8b17a6f31f9d).
25
+ * ## Configuring the ability to call an API
9
26
  *
10
- * ## Using the default Axios HTTP client
27
+ * The easiest way to configure the ability to `CallAnApi` is to provide the `baseURL` of your HTTP API,
28
+ * and rely on Serenity/JS to offer other sensible defaults:
11
29
  *
12
30
  * ```ts
13
31
  * import { actorCalled } from '@serenity-js/core'
@@ -19,30 +37,128 @@ import axios from 'axios';
19
37
  * CallAnApi.at('https://api.example.org/')
20
38
  * )
21
39
  * .attemptsTo(
40
+ * Send.a(GetRequest.to('/v1/users/2')), // GET https://api.example.org/v1/users/2
41
+ * Ensure.that(LastResponse.status(), equals(200)),
42
+ * )
43
+ * ```
44
+ *
45
+ * ### Resolving relative URLs
46
+ *
47
+ * Serenity/JS resolves request URLs using Node.js [WHATWG URL API](https://nodejs.org/api/url.html#new-urlinput-base).
48
+ * 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])`.
49
+ *
50
+ * Consider the following example:
51
+ *
52
+ * ```ts
53
+ * import { actorCalled } from '@serenity-js/core'
54
+ * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
55
+ * import { Ensure, equals } from '@serenity-js/assertions'
56
+ *
57
+ * await actorCalled('Apisitt')
58
+ * .whoCan(
59
+ * CallAnApi.at(baseURL)
60
+ * )
61
+ * .attemptsTo(
62
+ * Send.a(GetRequest.to(resourcePath)),
63
+ * Ensure.that(LastResponse.status(), equals(200)),
64
+ * )
65
+ * ```
66
+ *
67
+ * In the above example:
68
+ * - when `resourcePath` is defined as a full URL, it overrides the base URL
69
+ * - when `resourcePath` starts with a forward slash `/`, it replaces any path defined in the base URL
70
+ * - when `resourcePath` is not a full URL and doesn't start with a forward slash, it gets appended to the base URL
71
+ *
72
+ * | baseURL | resourcePath | result |
73
+ * | ----------------------------- | -------------------------- | ------------------------------------ |
74
+ * | `https://api.example.org/` | `/v1/users/2` | `https://api.example.org/v1/users/2` |
75
+ * | `https://example.org/api/v1/` | `users/2` | `https://example.org/api/v1/users/2` |
76
+ * | `https://example.org/api/v1/` | `/secure/oauth` | `https://example.org/secure/oauth` |
77
+ * | `https://v1.example.org/api/` | `https://v2.example.org/` | `https://v2.example.org/` |
78
+ *
79
+ * ### Using Axios configuration object
80
+ *
81
+ * When you need more control over how your Axios instance is configured, provide
82
+ * an [Axios configuration object](https://axios-http.com/docs/req_config). For example:
83
+ *
84
+ * ```ts
85
+ * import { actorCalled } from '@serenity-js/core'
86
+ * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
87
+ * import { Ensure, equals } from '@serenity-js/assertions'
88
+ *
89
+ * await actorCalled('Apisitt')
90
+ * .whoCan(
91
+ * CallAnApi.using({
92
+ * baseURL: 'https://api.example.org/',
93
+ * timeout: 30_000,
94
+ * // ... other configuration options
95
+ * })
96
+ * )
97
+ * .attemptsTo(
22
98
  * Send.a(GetRequest.to('/users/2')),
23
99
  * Ensure.that(LastResponse.status(), equals(200)),
24
100
  * )
25
101
  * ```
26
102
  *
27
- * ## Using Axios client with custom configuration
103
+ * ### Working with proxy servers
104
+ *
105
+ * `CallAnApi` uses [`proxy-from-env`](https://www.npmjs.com/package/proxy-from-env) to automatically
106
+ * detect proxy server configuration based on your [environment variables](https://www.npmjs.com/package/proxy-from-env#environment-variables).
107
+ *
108
+ * This default behaviour can be overridden by providing explicit proxy configuration:
109
+ *
110
+ * ```ts
111
+ * import { actorCalled } from '@serenity-js/core'
112
+ * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
113
+ * import { Ensure, equals } from '@serenity-js/assertions'
114
+ *
115
+ * await actorCalled('Apisitt')
116
+ * .whoCan(
117
+ * CallAnApi.using({
118
+ * baseURL: 'https://api.example.org/',
119
+ * proxy: {
120
+ * protocol: 'http',
121
+ * host: 'proxy.example.org',
122
+ * port: 9000,
123
+ * auth: { // `auth` is optional
124
+ * username: 'proxy-username',
125
+ * password: 'proxy-password',
126
+ * }
127
+ * }
128
+ * // ... other configuration options
129
+ * })
130
+ * )
131
+ * .attemptsTo(
132
+ * Send.a(GetRequest.to('/users/2')),
133
+ * Ensure.that(LastResponse.status(), equals(200)),
134
+ * )
135
+ * ```
136
+ *
137
+ * Please note that Serenity/JS uses [proxy-agents](https://github.com/TooTallNate/proxy-agents)
138
+ * and the approach described in ["Node.js Axios behind corporate proxies"](https://janmolak.com/node-js-axios-behind-corporate-proxies-8b17a6f31f9d)
139
+ * to work around [limited proxy support capabilities](https://github.com/axios/axios/issues?q=is%3Aissue+is%3Aopen+proxy) in Axios itself.
140
+ *
141
+ * ### Using Axios instance with proxy support
142
+ *
143
+ * To have full control over the Axios instance used by the ability to `CallAnApi`, you can create it yourself
144
+ * and inject it into the ability.
145
+ * This approach allows you to still benefit from automated proxy detection in configuration, while taking advantage
146
+ * of the many [Axios plugins](https://www.npmjs.com/search?q=axios).
28
147
  *
29
148
  * ```ts
30
149
  * import { actorCalled } from '@serenity-js/core'
31
150
  * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
32
151
  * import { Ensure, equals } from '@serenity-js/assertions'
33
152
  *
34
- * import * as axios from 'axios'
153
+ * import axios from 'axios'
154
+ * import axiosRetry from 'axios-retry'
35
155
  *
36
- * const axiosInstance = axios.create({
37
- * timeout: 5 * 1000,
38
- * headers: {
39
- * 'X-Custom-Api-Key': 'secret-key',
40
- * },
41
- * });
156
+ * const instance = axios.create({ baseURL 'https://api.example.org/' })
157
+ * axiosRetry(axios, { retries: 3 })
42
158
  *
43
159
  * await actorCalled('Apisitt')
44
160
  * .whoCan(
45
- * CallAnApi.using(axiosInstance),
161
+ * CallAnApi.using(instance)
46
162
  * )
47
163
  * .attemptsTo(
48
164
  * Send.a(GetRequest.to('/users/2')),
@@ -50,54 +166,228 @@ import axios from 'axios';
50
166
  * )
51
167
  * ```
52
168
  *
169
+ * ### Using raw Axios instance
170
+ *
171
+ * If you don't want Serenity/JS to enhance your Axios instance with proxy support, instantiate the ability to
172
+ * `CallAnApi` using its constructor directly.
173
+ * Note, however, that by using this approach you're taking the responsibility for all the aspects of configuring Axios.
174
+ *
175
+ * ```ts
176
+ * import { actorCalled } from '@serenity-js/core'
177
+ * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
178
+ * import { Ensure, equals } from '@serenity-js/assertions'
179
+ *
180
+ * import axios from 'axios'
181
+ * import axiosRetry from 'axios-retry'
182
+ *
183
+ * const instance = axios.create({ baseURL 'https://api.example.org/' })
184
+ * axiosRetry(axios, { retries: 3 })
185
+ *
186
+ * await actorCalled('Apisitt')
187
+ * .whoCan(
188
+ * new CallAnApi(instance) // using the constructor ensures your axios instance is not modified in any way.
189
+ * )
190
+ * .attemptsTo(
191
+ * // ...
192
+ * )
193
+ * ```
194
+ *
195
+ * ### Serenity/JS defaults
196
+ *
197
+ * When using {@apilink CallAnApi.at} or {@apilink CallAnApi.using} with a configuration object, Serenity/JS
198
+ * merges your [Axios request configuration](https://axios-http.com/docs/req_config) with the following defaults:
199
+ * - `timeout`: 10 seconds
200
+ *
201
+ *
202
+ * You can override them by specifying the given property in your configuration object, for example:
203
+ * ```ts
204
+ * import { actorCalled } from '@serenity-js/core'
205
+ * import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest'
206
+ * import { Ensure, equals } from '@serenity-js/assertions'
207
+ *
208
+ * await actorCalled('Apisitt')
209
+ * .whoCan(
210
+ * CallAnApi.using({
211
+ * baseURL: 'https://api.example.org/',
212
+ * timeout: 30_000
213
+ * })
214
+ * )
215
+ * .attemptsTo(
216
+ * Send.a(GetRequest.to('/users/2')),
217
+ * Ensure.that(LastResponse.status(), equals(200)),
218
+ * )
219
+ * ```
220
+ *
221
+ * ## Interacting with multiple APIs
222
+ *
223
+ * Some test scenarios might require you to interact with multiple HTTP APIs. With Serenity/JS you can do this
224
+ * using either API-specific actors, or by specifying full URLs when performing the requests.
225
+ *
226
+ * The following examples will assume that the test scenarios needs to interact with the following APIs:
227
+ * - `https://testdata.example.org/api/v1/`
228
+ * - `https://shop.example.org/api/v1/`
229
+ *
230
+ * Let's also assume that the `testdata` API allows the automation to manage the test data used by the `shop` API.
231
+ *
232
+ * ### Using API-specific actors
233
+ *
234
+ * To create API-specific actors, configure your [test runner](/handbook/test-runners/) with a {@apilink Cast}
235
+ * that gives your actors appropriate abilities based, for example, on their name:
236
+ *
237
+ * ```ts
238
+ * import { beforeEach } from 'mocha'
239
+ * import { Actor, Cast, engage } from '@serenity-js/core'
240
+ * import { CallAnApi } from '@serenity-js/rest'
241
+ *
242
+ * export class MyActors implements Cast {
243
+ * prepare(actor: Actor): Actor {
244
+ * switch(actor.name) {
245
+ * case 'Ted':
246
+ * return actor.whoCan(CallAnApi.at('https://testdata.example.org/api/v1/'))
247
+ * case 'Shelly':
248
+ * return actor.whoCan(CallAnApi.at('https://shop.example.org/api/v1/'))
249
+ * default:
250
+ * return actor;
251
+ * }
252
+ * }
253
+ * }
254
+ *
255
+ * beforeEach(() => engage(new MyActors()))
256
+ * ```
257
+ *
258
+ * Next, retrieve the appropriate actor in your test scenario using {@apilink actorCalled}, for example:
259
+ *
260
+ * ```ts
261
+ * import { describe, it, beforeEach } from 'mocha'
262
+ * import { actorCalled, engage } from '@serenity-js/core
263
+ * import { Send, GetRequest, PostRequest, LastResponse } from '@serenity-js/rest'
264
+ * import { Ensure, equals } from '@serenity-js/assertions'
265
+ *
266
+ * describe('Multi-actor API testing', () => {
267
+ * beforeEach(() => engage(new MyActors()))
268
+ *
269
+ * it('allows each actor to interact with their API', async () => {
270
+ *
271
+ * await actorCalled('Ted').attemptsTo(
272
+ * Send.a(PostRequest.to('products').with({ name: 'Apples', price: '£2.50' })),
273
+ * Ensure.that(LastResponse.status(), equals(201)),
274
+ * )
275
+ *
276
+ * await actorCalled('Shelly').attemptsTo(
277
+ * Send.a(GetRequest.to('?product=Apples')),
278
+ * Ensure.that(LastResponse.status(), equals(200)),
279
+ * Ensure.that(LastResponse.body(), equals([
280
+ * { name: 'Apples', price: '£2.50' }
281
+ * ])),
282
+ * )
283
+ * })
284
+ * })
285
+ * ```
286
+ *
287
+ * ### Using full URLs
288
+ *
289
+ * If you prefer to have a single actor interacting with multiple APIs, you can specify the full URL for every request:
290
+ *
291
+ * ```ts
292
+ * import { describe, it, beforeEach } from 'mocha'
293
+ * import { actorCalled, Cast, engage } from '@serenity-js/core
294
+ * import { CallAnApi, Send, GetRequest, PostRequest, LastResponse } from '@serenity-js/rest'
295
+ * import { Ensure, equals } from '@serenity-js/assertions'
296
+ *
297
+ * describe('Multi-actor API testing', () => {
298
+ * beforeEach(() => engage(
299
+ * Cast.where(actor => actor.whoCan(CallAnApi.using({})))
300
+ * ))
301
+ *
302
+ * it('allows each actor to interact with their API', async () => {
303
+ *
304
+ * await actorCalled('Alice').attemptsTo(
305
+ * Send.a(PostRequest.to('https://testdata.example.org/api/v1/products')
306
+ * .with({ name: 'Apples', price: '£2.50' })),
307
+ * Ensure.that(LastResponse.status(), equals(201)),
308
+ *
309
+ * Send.a(GetRequest.to('https://shop.example.org/api/v1/?product=Apples')),
310
+ * Ensure.that(LastResponse.status(), equals(200)),
311
+ * Ensure.that(LastResponse.body(), equals([
312
+ * { name: 'Apples', price: '£2.50' }
313
+ * ])),
314
+ * )
315
+ * })
316
+ * })
317
+ * ```
318
+ *
53
319
  * ## Learn more
54
- * - https://github.com/axios/axios
55
- * - https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
320
+ * - [Axios: Configuring requests](https://axios-http.com/docs/req_config)
321
+ * - [MDN: HTTP methods documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods)
56
322
  *
57
323
  * @group Abilities
58
324
  */
59
325
  export class CallAnApi extends Ability {
60
326
 
61
- /** @private */
62
327
  private lastResponse: AxiosResponse;
63
328
 
329
+ private static readonly defaults: CreateAxiosDefaults<any> = {
330
+ timeout: Duration.ofSeconds(10).inMilliseconds(),
331
+ };
332
+
64
333
  /**
65
- * Produces an {@apilink Ability|ability} to call a REST api at a specified baseUrl
334
+ * Produces an {@apilink Ability|ability} to call a REST API at a specified `baseURL`;
66
335
  *
67
- * Default timeout is set to 2s.
68
- *
69
- * Default request headers:
70
- * - `Accept`: `application/json,application/xml`
336
+ * This is the same as invoking `CallAnApi.using({ baseURL: 'https://example.org' })`
71
337
  *
72
338
  * @param baseURL
73
339
  */
74
- static at(baseURL: string): CallAnApi {
75
- return new CallAnApi(axios.create({
76
- baseURL,
77
- timeout: 2000,
78
- headers: { Accept: 'application/json,application/xml' },
79
- }));
340
+ static at(baseURL: URL | string): CallAnApi {
341
+ return CallAnApi.using({
342
+ baseURL: baseURL instanceof URL
343
+ ? baseURL.toString()
344
+ : baseURL
345
+ });
80
346
  }
81
347
 
82
348
  /**
83
- * Produces an {@apilink Ability|ability} to call a REST API using a given axios instance.
349
+ * Produces an {@apilink Ability|ability} to call an HTTP API using the given Axios instance,
350
+ * or an Axios request configuration object.
84
351
  *
85
- * Useful when you need to customise Axios to
86
- * [make it aware of proxies](https://janmolak.com/node-js-axios-behind-corporate-proxies-8b17a6f31f9d),
87
- * for example.
352
+ * When you provide an [Axios configuration object](https://axios-http.com/docs/req_config),
353
+ * it gets shallow-merged with the following defaults:
354
+ * - request timeout of 10 seconds
355
+ * - automatic proxy support based on
356
+ * your [environment variables](https://www.npmjs.com/package/proxy-from-env#environment-variables)
88
357
  *
89
- * #### Learn more
90
- * - [AxiosInstance](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L235-L238)
358
+ * When you provide an Axios instance, it's enhanced with proxy support and no other modifications are made.
91
359
  *
92
- * @param axiosInstance
360
+ * If you don't want Serenity/JS to augment or modify your Axios instance in any way,
361
+ * please use the {@apilink CallAnApi.constructor} directly.
362
+ *
363
+ * @param axiosInstanceOrConfig
93
364
  */
94
- static using(axiosInstance: AxiosInstance): CallAnApi {
95
- return new CallAnApi(axiosInstance);
365
+ static using(axiosInstanceOrConfig: AxiosInstance | AxiosRequestConfigDefaults): CallAnApi {
366
+
367
+ const axiosInstanceGiven = isAxiosInstance(axiosInstanceOrConfig);
368
+
369
+ const axiosInstance = axiosInstanceGiven
370
+ ? axiosInstanceOrConfig
371
+ : axios.create({
372
+ ...CallAnApi.defaults,
373
+ ...omit(axiosInstanceOrConfig, 'proxy'),
374
+ });
375
+
376
+ const proxyConfig: AxiosRequestConfigProxyDefaults | false | undefined = axiosInstanceGiven
377
+ ? axiosInstanceOrConfig.defaults.proxy
378
+ : axiosInstanceOrConfig.proxy;
379
+
380
+ const proxyOverrides = axiosProxyOverridesFor({
381
+ ...axiosInstance.defaults,
382
+ proxy: proxyConfig || undefined,
383
+ });
384
+
385
+ return new CallAnApi(withOverrides(axiosInstance, proxyOverrides));
96
386
  }
97
387
 
98
388
  /**
99
389
  * #### Learn more
100
- * - [AxiosInstance](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L235-L238)
390
+ * - [AxiosInstance](https://axios-http.com/docs/instance)
101
391
  *
102
392
  * @param axiosInstance
103
393
  * A pre-configured instance of the Axios HTTP client
@@ -112,7 +402,7 @@ export class CallAnApi extends Ability {
112
402
  * has been instantiated and given to the {@apilink Actor}.
113
403
  *
114
404
  * #### Learn more
115
- * - [AxiosRequestConfig](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L75-L113)
405
+ * - [AxiosRequestConfig](https://axios-http.com/docs/req_config)
116
406
  *
117
407
  * @param fn
118
408
  */
@@ -125,8 +415,8 @@ export class CallAnApi extends Ability {
125
415
  * Response will be cached and available via {@apilink mapLastResponse}
126
416
  *
127
417
  * #### Learn more
128
- * - [AxiosRequestConfig](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L75-L113)
129
- * - [AxiosResponse](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L133-L140)
418
+ * - [AxiosRequestConfig](https://axios-http.com/docs/req_config)
419
+ * - [AxiosResponse](https://axios-http.com/docs/res_schema)
130
420
  *
131
421
  * @param config
132
422
  * Axios request configuration, which can be used to override the defaults
@@ -144,7 +434,7 @@ export class CallAnApi extends Ability {
144
434
 
145
435
  return this.lastResponse;
146
436
  }
147
- catch(error) {
437
+ catch (error) {
148
438
  const description = `${ config.method.toUpperCase() } ${ url || config.url }`;
149
439
 
150
440
  switch (true) {
@@ -154,7 +444,7 @@ export class CallAnApi extends Ability {
154
444
  throw new TestCompromisedError(`A network error has occurred: ${ description }`, error);
155
445
  case error instanceof TypeError:
156
446
  throw new ConfigurationError(`Looks like there was an issue with Axios configuration`, error);
157
- case ! (error as AxiosError).response:
447
+ case !(error as AxiosError).response:
158
448
  throw new TestCompromisedError(`The API call has failed: ${ description }`, error);
159
449
  default:
160
450
  this.lastResponse = error.response;
@@ -168,9 +458,8 @@ export class CallAnApi extends Ability {
168
458
  * Resolves the final URL, based on the {@apilink AxiosRequestConfig} provided
169
459
  * and any defaults that the {@apilink AxiosInstance} has been configured with.
170
460
  *
171
- * #### Learn more
172
- * - [AxiosRequestConfig](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L75-L113)
173
- * - [AxiosInstance](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L235-L238)
461
+ * Note that unlike Axios, this method uses the Node.js [WHATWG URL API](https://nodejs.org/api/url.html#new-urlinput-base)
462
+ * to ensure URLs are correctly resolved.
174
463
  *
175
464
  * @param config
176
465
  */
@@ -187,7 +476,7 @@ export class CallAnApi extends Ability {
187
476
  * Useful when you need to extract a portion of the {@apilink AxiosResponse} object.
188
477
  *
189
478
  * #### Learn more
190
- * - [AxiosResponse](https://github.com/axios/axios/blob/v0.27.2/index.d.ts#L133-L140)
479
+ * - [AxiosResponse](https://axios-http.com/docs/res_schema)
191
480
  *
192
481
  * @param mappingFunction
193
482
  */
@@ -199,3 +488,21 @@ export class CallAnApi extends Ability {
199
488
  return mappingFunction(this.lastResponse);
200
489
  }
201
490
  }
491
+
492
+ function isAxiosInstance(axiosInstanceOrConfig: any): axiosInstanceOrConfig is AxiosInstance {
493
+ return axiosInstanceOrConfig
494
+ && (axiosInstanceOrConfig instanceof Axios || axiosInstanceOrConfig.defaults);
495
+ }
496
+
497
+ function withOverrides<Data = any>(axiosInstance: AxiosInstance, overrides: AxiosRequestConfig<Data>): AxiosInstance {
498
+ for (const [key, override] of Object.entries(overrides)) {
499
+ axiosInstance.defaults[key] = override;
500
+ }
501
+
502
+ return axiosInstance;
503
+ }
504
+
505
+ function omit<T extends object, K extends keyof T>(record: T, key: K): Omit<T, K> {
506
+ const { [key]: omitted_, ...rest } = record;
507
+ return rest;
508
+ }
@@ -1 +1,2 @@
1
+ export * from './AxiosRequestConfigDefaults';
1
2
  export * from './CallAnApi';