Skip to content

Commit

Permalink
feat: new healthcheck endpoint (#348)
Browse files Browse the repository at this point in the history
  • Loading branch information
luislhl authored Dec 5, 2023
1 parent c643c77 commit 07c6e58
Show file tree
Hide file tree
Showing 11 changed files with 809 additions and 0 deletions.
3 changes: 3 additions & 0 deletions __tests__/__fixtures__/http-fixtures.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export default {
'http://fake.txmining:8084/health': {
status: 'pass'
},
'/v1a/version': {
version: '0.38.4',
network: 'testnet-foxtrot',
Expand Down
1 change: 1 addition & 0 deletions __tests__/__fixtures__/settings-fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const defaultConfig = {
http_port: 8001,
network: 'testnet',
server: 'http://fakehost:8083/v1a/',
txMiningUrl: 'http://fake.txmining:8084/',
seeds: {
stub_seed:
'upon tennis increase embark dismiss diamond monitor face magnet jungle scout salute rural master shoulder cry juice jeans radar present close meat antenna mind',
Expand Down
310 changes: 310 additions & 0 deletions __tests__/healthcheck.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
import { HathorWallet } from '@hathor/wallet-lib';
import TestUtils from './test-utils';

const { initializedWallets } = require('../src/services/wallets.service');

const walletId = 'health_wallet';
const anotherWalletId = 'another_health_wallet';

describe('healthcheck api', () => {
beforeAll(async () => {
await TestUtils.startWallet({ walletId, preCalculatedAddresses: TestUtils.addresses });
await TestUtils.startWallet({
walletId: anotherWalletId,
preCalculatedAddresses: TestUtils.addresses
});
});

afterAll(async () => {
await TestUtils.stopWallet({ walletId });
await TestUtils.stopWallet({ walletId: anotherWalletId });
});

afterEach(async () => {
await TestUtils.stopMocks();
TestUtils.startMocks();
TestUtils.resetRequest();
});

describe('/health', () => {
it('should return 400 when the x-wallet-id is invalid', async () => {
const response = await TestUtils.request
.query({ wallet_ids: 'invalid' })
.get('/health');

expect(response.status).toBe(400);
expect(response.body).toStrictEqual({
success: false,
message: 'Invalid wallet id parameter: invalid',
});
});

it('should return 400 when no component is included', async () => {
const response = await TestUtils.request.get('/health');

expect(response.status).toBe(400);
expect(response.body).toStrictEqual({
success: false,
message: 'At least one component must be included in the health check',
});
});

it('should return 200 when all components are healthy', async () => {
const response = await TestUtils.request
.query({
include_tx_mining: true,
include_fullnode: true,
wallet_ids: `${walletId},${anotherWalletId}`
})
.get('/health');
expect(response.status).toBe(200);

expect(response.body).toStrictEqual({
status: 'pass',
description: 'Wallet-headless health',
checks: {
'Wallet health_wallet': [
{
componentName: 'Wallet health_wallet',
componentType: 'internal',
status: 'pass',
output: 'Wallet is ready',
time: expect.any(String),
},
],
'Wallet another_health_wallet': [
{
componentName: 'Wallet another_health_wallet',
componentType: 'internal',
status: 'pass',
output: 'Wallet is ready',
time: expect.any(String),
},
],
'Fullnode http://fakehost:8083/v1a/': [
{
componentName: 'Fullnode http://fakehost:8083/v1a/',
componentType: 'fullnode',
status: 'pass',
output: 'Fullnode is responding',
time: expect.any(String),
},
],
'TxMiningService http://fake.txmining:8084/': [
{
componentName: 'TxMiningService http://fake.txmining:8084/',
componentType: 'service',
status: 'pass',
output: 'Tx Mining Service is healthy',
time: expect.any(String),
},
],
}
});
});

it('should return 503 when the wallet is not ready', async () => {
const wallet = initializedWallets.get(walletId);
const originalIsReady = wallet.isReady;
const originalState = wallet.state;

wallet.isReady = () => false;
wallet.state = HathorWallet.SYNCING;

const response = await TestUtils.request
.query({ include_tx_mining: true, include_fullnode: true, wallet_ids: walletId })
.get('/health');
expect(response.status).toBe(503);

expect(response.body).toStrictEqual({
status: 'fail',
description: 'Wallet-headless health',
checks: {
'Wallet health_wallet': [
{
componentName: 'Wallet health_wallet',
componentType: 'internal',
status: 'fail',
output: 'Wallet is not ready. Current state: Syncing',
time: expect.any(String),
},
],
'Fullnode http://fakehost:8083/v1a/': [
{
componentName: 'Fullnode http://fakehost:8083/v1a/',
componentType: 'fullnode',
status: 'pass',
output: 'Fullnode is responding',
time: expect.any(String),
},
],
'TxMiningService http://fake.txmining:8084/': [
{
componentName: 'TxMiningService http://fake.txmining:8084/',
componentType: 'service',
status: 'pass',
output: 'Tx Mining Service is healthy',
time: expect.any(String),
},
],
}
});

wallet.isReady = originalIsReady;
wallet.state = originalState;
});

it('should return 503 when the fullnode is not healthy', async () => {
TestUtils.httpMock.onGet('/version').reply(503, { status: 'fail' });

const response = await TestUtils.request
.query({ include_tx_mining: true, include_fullnode: true, wallet_ids: walletId })
.get('/health');
expect(response.status).toBe(503);

expect(response.body).toStrictEqual({
status: 'fail',
description: 'Wallet-headless health',
checks: {
'Wallet health_wallet': [
{
componentName: 'Wallet health_wallet',
componentType: 'internal',
status: 'pass',
output: 'Wallet is ready',
time: expect.any(String),
},
],
'Fullnode http://fakehost:8083/v1a/': [
{
componentName: 'Fullnode http://fakehost:8083/v1a/',
componentType: 'fullnode',
status: 'fail',
output: 'Fullnode reported as unhealthy: {"status":"fail"}',
time: expect.any(String),
},
],
'TxMiningService http://fake.txmining:8084/': [
{
componentName: 'TxMiningService http://fake.txmining:8084/',
componentType: 'service',
status: 'pass',
output: 'Tx Mining Service is healthy',
time: expect.any(String),
},
],
}
});
});

it('should return 503 when the tx mining service is not healthy', async () => {
TestUtils.httpMock.onGet('http://fake.txmining:8084/health').reply(
503,
{ status: 'fail' }
);

const response = await TestUtils.request
.query({ include_tx_mining: true, include_fullnode: true, wallet_ids: walletId })
.get('/health');
expect(response.status).toBe(503);

expect(response.body).toStrictEqual({
status: 'fail',
description: 'Wallet-headless health',
checks: {
'Wallet health_wallet': [
{
componentName: 'Wallet health_wallet',
componentType: 'internal',
status: 'pass',
output: 'Wallet is ready',
time: expect.any(String),
},
],
'Fullnode http://fakehost:8083/v1a/': [
{
componentName: 'Fullnode http://fakehost:8083/v1a/',
componentType: 'fullnode',
status: 'pass',
output: 'Fullnode is responding',
time: expect.any(String),
},
],
'TxMiningService http://fake.txmining:8084/': [
{
componentName: 'TxMiningService http://fake.txmining:8084/',
componentType: 'service',
status: 'fail',
output: 'Tx Mining Service reported as unhealthy: {"status":"fail"}',
time: expect.any(String),
},
],
}
});
});

it('should not include the fullnode when the parameter is missing', async () => {
const response = await TestUtils.request
.query({ include_tx_mining: true, wallet_ids: walletId })
.get('/health');
expect(response.status).toBe(200);

expect(response.body).toStrictEqual({
status: 'pass',
description: 'Wallet-headless health',
checks: {
'Wallet health_wallet': [
{
componentName: 'Wallet health_wallet',
componentType: 'internal',
status: 'pass',
output: 'Wallet is ready',
time: expect.any(String),
},
],
'TxMiningService http://fake.txmining:8084/': [
{
componentName: 'TxMiningService http://fake.txmining:8084/',
componentType: 'service',
status: 'pass',
output: 'Tx Mining Service is healthy',
time: expect.any(String),
},
],
}
});
});

it('should not include the tx mining service when the parameter is missing', async () => {
const response = await TestUtils.request
.query({ include_fullnode: true, wallet_ids: walletId })
.get('/health');
expect(response.status).toBe(200);

expect(response.body).toStrictEqual({
status: 'pass',
description: 'Wallet-headless health',
checks: {
'Wallet health_wallet': [
{
componentName: 'Wallet health_wallet',
componentType: 'internal',
status: 'pass',
output: 'Wallet is ready',
time: expect.any(String),
},
],
'Fullnode http://fakehost:8083/v1a/': [
{
componentName: 'Fullnode http://fakehost:8083/v1a/',
componentType: 'fullnode',
status: 'pass',
output: 'Fullnode is responding',
time: expect.any(String),
},
],
}
});
});
});
});
7 changes: 7 additions & 0 deletions __tests__/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ class TestUtils {
httpMock.onGet('/thin_wallet/token').reply(200, httpFixtures['/thin_wallet/token']);
httpMock.onGet('/transaction').reply(200, httpFixtures['/transaction']);
httpMock.onGet('/getmininginfo').reply(200, httpFixtures['/getmininginfo']);
httpMock.onGet('http://fake.txmining:8084/health').reply(200, httpFixtures['http://fake.txmining:8084/health']);

// websocket mocks
wsMock.on('connection', socket => {
Expand Down Expand Up @@ -245,6 +246,12 @@ class TestUtils {
});
}

static resetRequest() {
// This can be used to reset the supertest agent and avoid interferences between tests,
// since everything is a singleton in this file
request = supertest.agent(server);
}

static stopMocks() {
httpMock.reset();
server.close();
Expand Down
Loading

0 comments on commit 07c6e58

Please sign in to comment.