Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support got http client #40

Merged
merged 15 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div align="center">
<h1>Expect HTTP Client Matchers</h1>

Additional expect matchers for http clients (e.g. Axios, got), supports `jest`, `vitest`, `expect`.
Additional expect matchers for http clients (e.g. Axios, got, or custom), supports `jest`, `vitest`, `expect`.

</div>

Expand All @@ -17,12 +17,16 @@ Additional expect matchers for http clients (e.g. Axios, got), supports `jest`,
- [Typescript](#typescript-1)
- [Jest](#jest)
- [Typescript](#typescript-2)
- [Asymmetric matchers](#asymmetric-matchers)
- [HTTP Clients](#http-clients)
- [Axios](#axios)
- [Setup](#setup)
- [Got](#got)
- [Setup](#setup-1)
- [Troubleshooting](#troubleshooting)
- [Error: _The `searchParameters` option does not exist. Use `searchParams` instead._](#error-_the-searchparameters-option-does-not-exist-use-searchparams-instead_)
- [Custom HTTP Client](#custom-http-client)
- [Configure](#configure)
- [Asymmetric matchers](#asymmetric-matchers)
- [API](#api)
- [.toBeSuccessful()](#tobesuccessful)
- [.toHave2xxStatus()](#tohave2xxstatus)
Expand Down Expand Up @@ -303,6 +307,38 @@ all the examples assume you have:
axios.defaults.validateStatus = () => true;
```

### Got

#### Setup

When using `got`, you should disable throwing on unsuccessful status codes as well

At the moment, `got` does not allow globally disabling throwing on unsuccessful status codes, you will need to do it per client/request
```js
import got from 'got';

// Use this client for all requests
const yourClient = got.extend({
// Don't throw on error
throwHttpErrors: false,

// I recommend disabling retry on failure as well
// retry: {
// limit: 0
// }
});
```

#### Troubleshooting

##### Error: _The `searchParameters` option does not exist. Use `searchParams` instead._

This is due to `jest-matcher-utils` bug (which `expect` and we use under the hood) when printing the request it evaluates every property getter
And `got` has a getter for `searchParameters` which throw an error as it's deprecated

See more here: [jest/jest#15280](https://github.com/jestjs/jest/issues/15280)


### Custom HTTP Client

If you are using a custom HTTP client or a client that is not supported, you can register it
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"concurrently": "^8.2.2",
"expect": "^29.7.0",
"fastify": "^4.28.1",
"got": "^14.4.2",
"husky": "^8.0.3",
"pretty-ansi": "^2.0.0",
"semantic-release": "^19.0.5"
Expand Down
5 changes: 3 additions & 2 deletions src/http-clients/adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*
* HTTP Client Adapter
* @abstract
* @template HttpClientResponse
*/
class HttpClientAdapter {
/**
Expand All @@ -17,13 +18,13 @@ class HttpClientAdapter {

/**
* The response object
* @type {unknown}
* @type {HttpClientResponse}
*/
response;

/**
*
* @param response
* @param {HttpClientResponse} response
* @protected
*/
constructor(response) {
Expand Down
11 changes: 7 additions & 4 deletions src/http-clients/axios-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ class AxiosHttpClientAdapter extends HttpClientAdapter {

/**
*
* @param _response
* @param response
* @return {CanAdapterHandle}
*/
static canHandle(_response) {
// TODO - implement
return 'maybe';
static canHandle(response) {
if (response.config) {
return 'maybe';
}

return 'no';
}

getUrl() {
Expand Down
49 changes: 49 additions & 0 deletions src/http-clients/got-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { HttpClientAdapter } = require('./adapter');

/**
* @typedef {import('got').Response} GotResponse
*/

/**
* @extends {HttpClientAdapter<GotResponse>}
*/
class GotHttpClientAdapter extends HttpClientAdapter {
static name = 'got';

/**
* @param {GotResponse} response
*/
constructor(response) {
super(response);
}

getUrl() {
return this.response.url;
}

/**
*
* @param response
* @return {CanAdapterHandle}
*/
static canHandle(response) {
if (response.timings) {
return 'maybe';
}

return 'no';
}

getStatusCode() {
return this.response.statusCode;
}

getHeaders() {
return this.response.headers;
}

getBody() {
return this.response.body;
}
}
module.exports = { GotHttpClientAdapter };
3 changes: 2 additions & 1 deletion src/http-clients/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const { AxiosHttpClientAdapter } = require('./axios-adapter');
const { GotHttpClientAdapter } = require('./got-adapter');

/**
*
* @type {(typeof HttpClientAdapter)[]}
*/
let adapters = [AxiosHttpClientAdapter];
let adapters = [AxiosHttpClientAdapter, GotHttpClientAdapter];

/**
*
Expand Down
32 changes: 32 additions & 0 deletions test/helpers/can-test-snapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const { printDiffOrStringify } = require('jest-matcher-utils');

// Due to Jest bug with accessing getters that throw errors
// and failing the matcher we can't test this
// https://github.com/jestjs/jest/issues/15280
function canTestSnapshot() {
try {
printDiffOrStringify(
{},
{
get a() {
throw new Error('simulate error');
},
},
'expected',
'received',
false,
);

// If it does not throw an error that we can create snapshot when there is a getter that throws an error
return true;
} catch (e) {
return false;
}
}
module.exports = {
canTestSnapshot,

shouldTestAsymmetricMatcherErrorsSnapshot(testClient) {
return testClient.name === 'got' && canTestSnapshot();
},
};
62 changes: 62 additions & 0 deletions test/helpers/override-http-client-defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const axios = require('axios');

/**
* @type {import('got').Got | undefined}
*/
let got;

async function getGot() {
if (!got) {
const defaultGot = (await import('got')).default;

// Avoid throwing on error
got = defaultGot.extend({
// Don't throw on error
throwHttpErrors: false,

// Don't retry
retry: {
limit: 0,
},

hooks: {
afterResponse: [
(response) => {
// Delete the following headers as they are dynamic and the test snapshot won't match
delete response.headers['keep-alive'];
delete response.headers['date'];

return response;
},
],
},

mutableDefaults: true,
});
}

return got;
}

function fixHttpClients() {
// Don't throw on error
axios.defaults.validateStatus = () => true;

// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Delete the following headers as they are dynamic and the test snapshot won't match
delete response.headers['keep-alive'];
delete response.headers['date'];

return response;
});

getGot().catch((e) => {
console.error('failed to get got', e);
});
}

module.exports = {
getGot,
fixHttpClients,
};
74 changes: 74 additions & 0 deletions test/helpers/supported-clients/got-adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
const { getGot } = require('../override-http-client-defaults');

const gotTestAdapterClient = {
name: 'got',
async request(url, config) {
return (await getGot()).request({
url,
json: typeof config?.data === 'object' ? config?.data : undefined,
body: typeof config?.data !== 'object' ? config?.data : undefined,
method: config.method.toUpperCase(),
headers: config.headers,
searchParams: config.params,
});
},
async get(url, config) {
return (await getGot()).get(url, {
json: typeof config?.data === 'object' ? config?.data : undefined,
body: typeof config?.data !== 'object' ? config?.data : undefined,
headers: config?.headers,
searchParams: config?.params,
});
},
async delete(url, config) {
return (await getGot()).delete(url, {
json: typeof config?.data === 'object' ? config?.data : undefined,
body: typeof config?.data !== 'object' ? config?.data : undefined,
headers: config?.headers,
searchParams: config?.params,
});
},
async head(url, config) {
return (await getGot()).head(url, {
json: typeof config?.data === 'object' ? config?.data : undefined,
body: typeof config?.data !== 'object' ? config?.data : undefined,
headers: config?.headers,
searchParams: config?.params,
});
},
async options(url, config) {
return (await getGot()).options(url, {
json: typeof config?.data === 'object' ? config?.data : undefined,
body: typeof config?.data !== 'object' ? config?.data : undefined,
headers: config?.headers,
searchParams: config?.params,
});
},
async post(url, data, config) {
const response = await (await getGot()).post(url, {
json: typeof data === 'object' ? data : undefined,
body: typeof data !== 'object' ? data : undefined,
headers: config?.headers,
searchParams: config?.params,
});
return response;
},
async put(url, data, config) {
return (await getGot()).put(url, {
json: typeof data === 'object' ? data : undefined,
body: typeof data !== 'object' ? data : undefined,
headers: config?.headers,
searchParams: config?.params,
});
},
async patch(url, data, config) {
return (await getGot()).patch(url, {
json: typeof data === 'object' ? data : undefined,
body: typeof data !== 'object' ? data : undefined,
headers: config?.headers,
searchParams: config?.params,
});
},
};

module.exports = { gotTestAdapterClient };
3 changes: 2 additions & 1 deletion test/helpers/supported-clients/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const { axiosTestAdapterClient } = require('./axios-adapter');
const { gotTestAdapterClient } = require('./got-adapter.js');

/**
* @type {TestAdapterClient[]}
*/
const testClients = [axiosTestAdapterClient];
const testClients = [axiosTestAdapterClient, gotTestAdapterClient];

module.exports = {
testClients,
Expand Down
Loading
Loading