Skip to content

Commit

Permalink
Merge pull request #5538 from LiskHQ/5254_http_api_get_peers
Browse files Browse the repository at this point in the history
Add http-api for GET api/peers?limit=xxx&offset=yyy&state=zzz endpoint - Closes #5254
  • Loading branch information
shuse2 committed Jul 13, 2020
2 parents 2ad592e + 51bee53 commit efe9e1f
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 1 deletion.
Expand Up @@ -16,6 +16,7 @@ import * as transactions from './transactions';
import * as accounts from './accounts';
import * as node from './node';
import * as blocks from './blocks';
import * as peers from './peers';

export * from './hello';
export { accounts, blocks, node, transactions };
export { accounts, blocks, node, transactions, peers };
@@ -0,0 +1,88 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
import { Request, Response, NextFunction } from 'express';
import { validator, LiskValidationError } from '@liskhq/lisk-validator';
import { BaseChannel } from 'lisk-framework';
import { paginateList } from '../utils';

const getPeerSchema = {
type: 'object',
properties: {
limit: {
type: 'string',
format: 'uint64',
description: 'Number of peers to be returned',
},
offset: {
type: 'string',
format: 'uint64',
description: 'Offset to get peers after a specific point in a peer list',
},
state: {
type: 'string',
enum: ['connected', 'disconnected'],
},
},
default: {
limit: 100,
offset: 0,
state: 'connected',
},
};

enum PeerState {
connected = 'connected',
disconnected = 'disconnected',
}

interface PeerInfo {
readonly ipAddress: string;
readonly port: number;
readonly networkId: string;
readonly networkVersion: string;
readonly nonce: string;
readonly options: { [key: string]: unknown };
}

export const getPeers = (channel: BaseChannel) => async (
req: Request,
res: Response,
next: NextFunction,
): Promise<void> => {
const errors = validator.validate(getPeerSchema, req.query);

// 400 - Malformed query or parameters
if (errors.length) {
res.status(400).send({
errors: [{ message: new LiskValidationError([...errors]).message }],
});
return;
}
const { limit = 100, offset = 0, state = PeerState.connected } = req.query;

try {
let peers;
if (state === PeerState.disconnected) {
peers = await channel.invoke<ReadonlyArray<PeerInfo>>('app:getDisconnectedPeers');
} else {
peers = await channel.invoke<ReadonlyArray<PeerInfo>>('app:getConnectedPeers');
}

peers = paginateList(peers, +limit, +offset);

res.status(200).send(peers);
} catch (err) {
next(err);
}
};
Expand Up @@ -123,5 +123,6 @@ export class HTTPAPIPlugin extends BasePlugin {
'/api/node/transactions',
controllers.node.getTransactions(this._channel, this.codec),
);
this._app.get('/api/peers', controllers.peers.getPeers(this._channel));
}
}
25 changes: 25 additions & 0 deletions framework-plugins/lisk-framework-http-api-plugin/src/utils.ts
@@ -0,0 +1,25 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/

export const paginateList = <T>(
list: ReadonlyArray<T>,
limit = 100,
offset = 0,
): ReadonlyArray<T> => {
if (offset === 0) {
return list.slice(0, Math.min(limit, list.length));
}

return list.slice(offset, Math.min(limit + offset, list.length));
};
@@ -0,0 +1,129 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/
import { Application } from 'lisk-framework';
import axios from 'axios';
import { when } from 'jest-when';
import { createApplication, closeApplication, getURL, callNetwork } from './utils/application';
import { generatePeers } from './utils/peers';

describe('Peers endpoint', () => {
let app: Application;
const peers = generatePeers();

beforeAll(async () => {
app = await createApplication('peers');
});

afterAll(async () => {
await closeApplication(app);
});

describe('/api/peers', () => {
it('should respond with 100 connected peers as limit has 100 default value', async () => {
// Arrange
app['_channel'].invoke = jest.fn();
// Mock channel invoke only when app:getConnectedPeers is called
when(app['_channel'].invoke)
.calledWith('app:getConnectedPeers')
.mockResolvedValue(peers as never);

// Act
const { response, status } = await callNetwork(axios.get(getURL('/api/peers')));

// Assert
expect(response).toEqual(peers.slice(0, 100));
expect(status).toBe(200);
});

it('should respond with all disconnected peers when all query parameters are passed', async () => {
// Arrange
app['_channel'].invoke = jest.fn();
// Mock channel invoke only when app:getDisconnectedPeers is called
when(app['_channel'].invoke)
.calledWith('app:getDisconnectedPeers')
.mockResolvedValue(peers as never);

// Act
const { response, status } = await callNetwork(
axios.get(getURL('/api/peers?state=disconnected&limit=100&offset=2')),
);

// Assert
expect(response).toEqual(peers.slice(2, 102));
expect(status).toBe(200);
});

it('should throw 500 error when channel.invoke fails', async () => {
// Arrange
app['_channel'].invoke = jest.fn();
// Mock channel invoke only when app:getConnectedPeers is called
when(app['_channel'].invoke)
.calledWith('app:getConnectedPeers')
.mockRejectedValue(new Error('test') as never);
const { response, status } = await callNetwork(axios.get(getURL('/api/peers')));
// Assert
expect(status).toBe(500);
expect(response).toEqual({
errors: [
{
message: 'test',
},
],
});
});

it('should respond with 400 and error message when passed incorrect state value', async () => {
const { response, status } = await callNetwork(axios.get(getURL('/api/peers?state=xxx')));
// Assert
expect(status).toBe(400);
expect(response).toEqual({
errors: [
{
message:
'Lisk validator found 1 error[s]:\nshould be equal to one of the allowed values',
},
],
});
});

it('should respond with 400 and error message when passed incorrect limit value', async () => {
const { response, status } = await callNetwork(axios.get(getURL('/api/peers?limit=123xy')));
// Assert
expect(status).toBe(400);
expect(response).toEqual({
errors: [
{
message:
'Lisk validator found 1 error[s]:\nProperty \'.limit\' should match format "uint64"',
},
],
});
});

it('should respond with 400 and error message when passed incorrect offset value', async () => {
// Act
const { response, status } = await callNetwork(axios.get(getURL('/api/peers?offset=123xy')));
// Assert
expect(status).toBe(400);
expect(response).toEqual({
errors: [
{
message:
'Lisk validator found 1 error[s]:\nProperty \'.offset\' should match format "uint64"',
},
],
});
});
});
});
@@ -0,0 +1,28 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/

export const generatePeers = (numOfPeers = 200) => {
const peers = [];
for (let i = 0; i < numOfPeers; i += 1) {
peers.push({
ipAddress: `1.1.1.${i}`,
port: 1000 + i,
networkId: 'networkId',
networVersion: '1.1',
nonce: `nonce${i}`,
});
}

return peers;
};
@@ -0,0 +1,32 @@
/*
* Copyright © 2020 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/

import { paginateList } from '../../src/utils';

describe('paginateList', () => {
const exampleArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const exampleArrayLength = exampleArray.length;

it('should return all elements when limit and offset are not provided', () => {
expect(paginateList(exampleArray)).toEqual(exampleArray);
});

it('should return only first 5 elements when limit is 5', () => {
expect(paginateList(exampleArray, 5)).toEqual(exampleArray.slice(0, 5));
});

it('should return elements after 5th element when offset and limit are 5', () => {
expect(paginateList(exampleArray, 5, 5)).toEqual(exampleArray.slice(5, exampleArrayLength));
});
});

0 comments on commit efe9e1f

Please sign in to comment.