@getanthill/datastore
Version:
Event-Sourced Datastore
394 lines (319 loc) • 9.51 kB
text/typescript
import util from 'util';
import Core from './Core';
import _ from 'lodash';
import { AxiosError } from 'axios';
describe('sdk/Core', () => {
let client;
beforeEach(() => {
client = new Core();
client._axios = {
request: jest.fn(),
defaults: {},
};
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('constructor', () => {
it('creates a new client with default configuration', () => {
client = new Core();
expect(client._config).toEqual({
baseUrl: 'http://localhost:3001',
timeout: 10000,
token: 'token',
debug: false,
maxRetry: 3,
retriableMethods: ['get', 'head', 'options'],
retriableErrors: ['socket hang up'],
});
});
it('creates a new client with configuration defined in the signature', () => {
client = new Core({
baseUrl: 'https://datastore.org',
token: 'private_token',
});
expect(client._config).toMatchObject({
baseUrl: 'https://datastore.org',
token: 'private_token',
});
});
it('creates a new client with axios instanciated', () => {
client = new Core();
expect(client._axios).toHaveProperty('request');
expect(client._axios).toHaveProperty('get');
expect(client._axios).toHaveProperty('post');
expect(client._axios).toHaveProperty('put');
expect(client._axios).toHaveProperty('delete');
expect(client._axios.defaults.headers).toMatchObject({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: 'token',
});
});
it('creates a new client with telemetry enabled', () => {
client = new Core({
telemetry: { logger: true },
});
expect(client._telemetry).toEqual({ logger: true });
});
});
describe('#paramsSerializer', () => {
it('serializes simple query params correctly', () => {
expect(
Core.paramsSerializer({
a: 1,
}),
).toEqual('q=' + encodeURIComponent(JSON.stringify({ a: 1 })) + '&a=1');
});
it('serializes nested object correctly', () => {
expect(
Core.paramsSerializer({
a: {
b: 1,
},
}),
).toEqual(
'q=' +
encodeURIComponent(
JSON.stringify({
a: {
b: 1,
},
}),
) +
'&a%5Bb%5D=1',
);
});
it('serializes arrays correctly', () => {
expect(
Core.paramsSerializer({
a: ['b', 'c', 'd'],
}),
).toEqual(
'q=' +
encodeURIComponent(
JSON.stringify({
a: ['b', 'c', 'd'],
}),
) +
'&a%5B%5D=b&a%5B%5D=c&a%5B%5D=d',
);
});
it('sends the empty array to the datastore', () => {
expect(
Core.paramsSerializer({
account_id: [],
}),
).toEqual(
'q=' + encodeURIComponent(JSON.stringify({ account_id: [] })) + '&',
);
});
});
describe('#inspect', () => {
it('returns the object only if `debug=false`', () => {
client._config.debug = false;
console.log = jest.fn();
expect(client.inspect({ a: 1 })).toEqual({ a: 1 });
expect(console.log).toHaveBeenCalledTimes(0);
});
it('returns the object and log details of object if `debug=true`', () => {
client._config.debug = true;
console.log = jest.fn();
expect(client.inspect({ a: 1 })).toEqual({ a: 1 });
expect(console.log).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith(
util.inspect({ a: 1 }, false, null, true),
);
});
});
describe('#setTimeout', () => {
it('forces the value of the axios timeout', () => {
client.setTimeout(1200);
expect(client._axios.defaults.timeout).toEqual(1200);
});
});
describe('#getPath', () => {
it('returns the path fragments', () => {
expect(client.getPath('a', 'b')).toEqual('/api/a/b');
});
});
describe('#request', () => {
it('performs a request on the client', async () => {
await client.request({
method: 'get',
url: '/heartbeat',
});
expect(client._axios.request).toHaveBeenCalledTimes(1);
expect(client._axios.request).toHaveBeenLastCalledWith({
method: 'get',
url: '/heartbeat',
headers: {
'force-primary': undefined,
},
});
});
it('throws an error in case of exception', async () => {
client.inspect = jest.fn();
client._axios = {
request: jest.fn().mockImplementation(() => {
const err = new Error('Ooops');
// @ts-ignore
err.response = {
data: { code: 500, message: 'Internal Server Error' },
};
throw err;
}),
};
let error;
try {
await client.request({
method: 'get',
url: '/heartbeat',
});
} catch (err) {
error = err;
}
expect(error).toBeInstanceOf(Error);
expect(error.message).toEqual('Ooops');
expect(client.inspect).toHaveBeenCalledTimes(1);
expect(client.inspect).toHaveBeenCalledWith(error);
});
it('throws an error in case of exception but do not inspect if not an HTTP Error', async () => {
client.inspect = jest.fn();
client._axios = {
request: jest.fn().mockImplementation(() => {
const err = new Error('Ooops');
throw err;
}),
};
let error;
try {
await client.request({
method: 'get',
url: '/heartbeat',
});
} catch (err) {
error = err;
}
expect(error).toBeInstanceOf(Error);
expect(error.message).toEqual('Ooops');
expect(client.inspect).toHaveBeenCalledTimes(1);
});
});
describe('#responseInterceptor', () => {
it('throws the error in case of missing request configuration', async () => {
const error = new AxiosError();
let _error;
try {
client.responseInterceptor(error);
} catch (err) {
_error = err;
}
expect(_error).toEqual(error);
});
it('throws the error in case of non retriable method', async () => {
const error = new AxiosError();
// @ts-ignore
error.config = {
method: 'post',
};
let _error;
try {
client.responseInterceptor(error);
} catch (err) {
_error = err;
}
expect(_error).toEqual(error);
});
it('throws the error in case if the max retry number exhausted', async () => {
const error = new AxiosError('socket hang up');
// @ts-ignore
error.config = {
// @ts-ignore
_retry: 3,
};
let _error;
try {
client.responseInterceptor(error);
} catch (err) {
_error = err;
}
expect(_error).toEqual(error);
});
it('throws the error in case of non matching error message', async () => {
const error = new AxiosError('non matching');
// @ts-ignore
error.config = {
method: 'get',
};
let _error;
try {
client.responseInterceptor(error);
} catch (err) {
_error = err;
}
expect(_error).toEqual(error);
});
it('increments the `_retry` count in case of retriable request', async () => {
const error = new AxiosError('socket hang up');
// @ts-ignore
error.config = {
method: 'get',
};
client._axios = jest.fn();
client.responseInterceptor(error);
expect(error.config).toHaveProperty('_retry', 1);
});
it('increments the `_retry` count in case of retriable request with special `_q` stringified param', async () => {
const error = new AxiosError('socket hang up');
// @ts-ignore
error.config = {
method: 'get',
params: {
_q: JSON.stringify({ hello: 'world' }),
},
};
client._axios = jest.fn();
client.responseInterceptor(error);
expect(error.config).toHaveProperty('_retry', 1);
});
it('retries the request if retriable', async () => {
const error = new AxiosError('socket hang up');
// @ts-ignore
error.config = {
method: 'get',
};
client._axios = jest.fn();
client.responseInterceptor(error);
expect(client._axios).toHaveBeenCalledWith(error.config);
});
it('retries the request if retriable with a custom method', async () => {
client = new Core({
retriableMethods: ['post'],
});
const error = new AxiosError('socket hang up');
// @ts-ignore
error.config = {
method: 'post',
};
client._axios = jest.fn();
client.responseInterceptor(error);
expect(client._axios).toHaveBeenCalledWith(error.config);
});
it('retries the request if retriable with a custom retry max value', async () => {
client = new Core({
maxRetry: 5,
});
const error = new AxiosError('socket hang up');
// @ts-ignore
error.config = {
method: 'get',
// @ts-ignore
_retry: 4,
};
client._axios = jest.fn();
client.responseInterceptor(error);
expect(client._axios).toHaveBeenCalledWith(error.config);
});
});
});