Skip to content

Commit

Permalink
Add NodeHttpClient and FetchHttpClient tests for retry logic (#1154)
Browse files Browse the repository at this point in the history
## Description

- Adds tests for `NodeHttpClient` and `FetchHttpClient` to test network
retries for the FGA module
- Removes retry test cases from FGA module unit tests

## Documentation

Does this require changes to the WorkOS Docs? E.g. the [API
Reference](https://workos.com/docs/reference) or code snippets need
updates.

```
[ ] Yes
```

If yes, link a related docs PR and add a docs maintainer as a reviewer.
Their approval is required.
  • Loading branch information
stanleyphu authored Nov 6, 2024
1 parent 4079e10 commit d6e32e1
Show file tree
Hide file tree
Showing 4 changed files with 427 additions and 692 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"jest": "29.6.2",
"jest-environment-miniflare": "^2.14.2",
"jest-fetch-mock": "^3.0.3",
"nock": "^13.5.5",
"prettier": "2.8.8",
"supertest": "6.3.3",
"ts-jest": "29.1.3",
Expand Down
227 changes: 227 additions & 0 deletions src/common/net/fetch-client.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import fetch from 'jest-fetch-mock';
import { fetchOnce, fetchURL } from '../../common/utils/test-utils';
import { FetchHttpClient } from './fetch-client';

const fetchClient = new FetchHttpClient('https://test.workos.com', {
headers: {
Authorization: `Bearer sk_test`,
'User-Agent': 'test-fetch-client',
},
});

describe('Fetch client', () => {
beforeEach(() => fetch.resetMocks());

describe('fetchRequestWithRetry', () => {
it('get for FGA path should call fetchRequestWithRetry and return response', async () => {
fetchOnce({ data: 'response' });
const mockFetchRequestWithRetry = jest.spyOn(
FetchHttpClient.prototype as any,
'fetchRequestWithRetry',
);

const response = await fetchClient.get('/fga/v1/resources', {});

expect(mockFetchRequestWithRetry).toHaveBeenCalledTimes(1);
expect(fetchURL()).toBe('https://test.workos.com/fga/v1/resources');
expect(await response.toJSON()).toEqual({ data: 'response' });
});

it('post for FGA path should call fetchRequestWithRetry and return response', async () => {
fetchOnce({ data: 'response' });
const mockFetchRequestWithRetry = jest.spyOn(
FetchHttpClient.prototype as any,
'fetchRequestWithRetry',
);

const response = await fetchClient.post('/fga/v1/resources', {}, {});

expect(mockFetchRequestWithRetry).toHaveBeenCalledTimes(1);
expect(fetchURL()).toBe('https://test.workos.com/fga/v1/resources');
expect(await response.toJSON()).toEqual({ data: 'response' });
});

it('put for FGA path should call fetchRequestWithRetry and return response', async () => {
fetchOnce({ data: 'response' });
const mockFetchRequestWithRetry = jest.spyOn(
FetchHttpClient.prototype as any,
'fetchRequestWithRetry',
);

const response = await fetchClient.put(
'/fga/v1/resources/user/user-1',
{},
{},
);

expect(mockFetchRequestWithRetry).toHaveBeenCalledTimes(1);
expect(fetchURL()).toBe(
'https://test.workos.com/fga/v1/resources/user/user-1',
);
expect(await response.toJSON()).toEqual({ data: 'response' });
});

it('delete for FGA path should call fetchRequestWithRetry and return response', async () => {
fetchOnce({ data: 'response' });
const mockFetchRequestWithRetry = jest.spyOn(
FetchHttpClient.prototype as any,
'fetchRequestWithRetry',
);

const response = await fetchClient.delete(
'/fga/v1/resources/user/user-1',
{},
);

expect(mockFetchRequestWithRetry).toHaveBeenCalledTimes(1);
expect(fetchURL()).toBe(
'https://test.workos.com/fga/v1/resources/user/user-1',
);
expect(await response.toJSON()).toEqual({ data: 'response' });
});

it('should retry request on 500 status code', async () => {
fetchOnce(
{},
{
status: 500,
},
);
fetchOnce({ data: 'response' });
const mockShouldRetryRequest = jest.spyOn(
FetchHttpClient.prototype as any,
'shouldRetryRequest',
);
const mockSleep = jest.spyOn(fetchClient, 'sleep');
mockSleep.mockImplementation(() => Promise.resolve());

const response = await fetchClient.get('/fga/v1/resources', {});

expect(mockShouldRetryRequest).toHaveBeenCalledTimes(2);
expect(mockSleep).toHaveBeenCalledTimes(1);
expect(await response.toJSON()).toEqual({ data: 'response' });
});

it('should retry request on 502 status code', async () => {
fetchOnce(
{},
{
status: 502,
},
);
fetchOnce({ data: 'response' });
const mockShouldRetryRequest = jest.spyOn(
FetchHttpClient.prototype as any,
'shouldRetryRequest',
);
const mockSleep = jest.spyOn(fetchClient, 'sleep');
mockSleep.mockImplementation(() => Promise.resolve());

const response = await fetchClient.get('/fga/v1/resources', {});

expect(mockShouldRetryRequest).toHaveBeenCalledTimes(2);
expect(mockSleep).toHaveBeenCalledTimes(1);
expect(await response.toJSON()).toEqual({ data: 'response' });
});

it('should retry request on 504 status code', async () => {
fetchOnce(
{},
{
status: 504,
},
);
fetchOnce({ data: 'response' });
const mockShouldRetryRequest = jest.spyOn(
FetchHttpClient.prototype as any,
'shouldRetryRequest',
);
const mockSleep = jest.spyOn(fetchClient, 'sleep');
mockSleep.mockImplementation(() => Promise.resolve());

const response = await fetchClient.get('/fga/v1/resources', {});

expect(mockShouldRetryRequest).toHaveBeenCalledTimes(2);
expect(mockSleep).toHaveBeenCalledTimes(1);
expect(await response.toJSON()).toEqual({ data: 'response' });
});

it('should retry request up to 3 times on retryable status code', async () => {
fetchOnce(
{},
{
status: 500,
},
);
fetchOnce(
{},
{
status: 502,
},
);
fetchOnce(
{},
{
status: 504,
},
);
fetchOnce(
{},
{
status: 504,
},
);
const mockShouldRetryRequest = jest.spyOn(
FetchHttpClient.prototype as any,
'shouldRetryRequest',
);
const mockSleep = jest.spyOn(fetchClient, 'sleep');
mockSleep.mockImplementation(() => Promise.resolve());

await expect(
fetchClient.get('/fga/v1/resources', {}),
).rejects.toThrowError('Gateway Timeout');

expect(mockShouldRetryRequest).toHaveBeenCalledTimes(4);
expect(mockSleep).toHaveBeenCalledTimes(3);
});

it('should not retry requests and throw error with non-retryable status code', async () => {
fetchOnce(
{},
{
status: 400,
},
);
const mockShouldRetryRequest = jest.spyOn(
FetchHttpClient.prototype as any,
'shouldRetryRequest',
);

await expect(
fetchClient.get('/fga/v1/resources', {}),
).rejects.toThrowError('Bad Request');

expect(mockShouldRetryRequest).toHaveBeenCalledTimes(1);
});

it('should retry request on TypeError', async () => {
fetchOnce({ data: 'response' });
const mockFetchRequest = jest.spyOn(
FetchHttpClient.prototype as any,
'fetchRequest',
);
mockFetchRequest.mockImplementationOnce(() => {
throw new TypeError('Network request failed');
});
const mockSleep = jest.spyOn(fetchClient, 'sleep');
mockSleep.mockImplementation(() => Promise.resolve());

const response = await fetchClient.get('/fga/v1/resources', {});

expect(mockFetchRequest).toHaveBeenCalledTimes(2);
expect(mockSleep).toHaveBeenCalledTimes(1);
expect(await response.toJSON()).toEqual({ data: 'response' });
});
});
});
Loading

0 comments on commit d6e32e1

Please sign in to comment.