Skip to content
This repository was archived by the owner on Jun 11, 2024. It is now read-only.

Commit f641009

Browse files
authored
Unit test ChainEndpoint (#9168)
* ✅ ChainEndpoint.getBlockByID * ✅ ChainEndpoint.getBlocksByIDs * ✅ ChainEndpoint.getBlockByHeight * ✅ ChainEndpoint.getBlocksByHeightBetween * ✅ ChainEndpoint.getLastBlock * ✅ ChainEndpoint.getTransactionByID * ♻️ ✅ ChainEndpoint.getTransactionsByIDs * ✅ ChainEndpoint.getTransactionsByHeight * ✅ ChainEndpoint.getAssetsByHeight * ✅ ChainEndpoint.areHeadersContradicting * ♻️ Redefines test scenario
1 parent 175f817 commit f641009

File tree

2 files changed

+304
-3
lines changed

2 files changed

+304
-3
lines changed

Diff for: framework/src/engine/endpoint/chain.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export class ChainEndpoint {
148148
throw new Error('Invalid parameters. ids must be a non empty array.');
149149
}
150150
if (!ids.every(id => isHexString(id))) {
151-
throw new Error('Invalid parameters. id must a valid hex string.');
151+
throw new Error('Invalid parameters. id must be a valid hex string.');
152152
}
153153
const transactions = [];
154154
try {

Diff for: framework/test/unit/engine/endpoint/chain.spec.ts

+303-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,16 @@
1212
* Removal or modification of this copyright notice is prohibited.
1313
*/
1414

15-
import { Event, StateStore } from '@liskhq/lisk-chain';
15+
import {
16+
Block,
17+
BlockAssets,
18+
BlockHeader,
19+
Event,
20+
StateStore,
21+
Transaction,
22+
} from '@liskhq/lisk-chain';
1623
import { utils } from '@liskhq/lisk-cryptography';
17-
import { Batch, Database, InMemoryDatabase } from '@liskhq/lisk-db';
24+
import { Batch, Database, InMemoryDatabase, NotFoundError } from '@liskhq/lisk-db';
1825
import {
1926
EMPTY_KEY,
2027
MODULE_STORE_PREFIX_BFT,
@@ -24,20 +31,64 @@ import {
2431
import { bftParametersSchema, bftVotesSchema } from '../../../../src/engine/bft/schemas';
2532
import { ChainEndpoint } from '../../../../src/engine/endpoint/chain';
2633
import { createRequestContext } from '../../../utils/mocks/endpoint';
34+
import * as bftUtils from '../../../../src/engine/bft/utils';
2735

2836
describe('Chain endpoint', () => {
2937
const DEFAULT_INTERVAL = 10;
3038
let stateStore: StateStore;
3139
let endpoint: ChainEndpoint;
3240
let db: InMemoryDatabase;
41+
const transaction = new Transaction({
42+
module: 'token',
43+
command: 'transfer',
44+
fee: BigInt(613000),
45+
params: utils.getRandomBytes(100),
46+
nonce: BigInt(2),
47+
senderPublicKey: utils.getRandomBytes(32),
48+
signatures: [utils.getRandomBytes(64)],
49+
});
50+
const blockAsset = new BlockAssets();
51+
const getBlockAttrs = (attrs?: Record<string, unknown>) => ({
52+
version: 1,
53+
timestamp: 1009988,
54+
height: 1009988,
55+
previousBlockID: Buffer.from('4a462ea57a8c9f72d866c09770e5ec70cef18727', 'hex'),
56+
stateRoot: Buffer.from('7f9d96a09a3fd17f3478eb7bef3a8bda00e1238b', 'hex'),
57+
transactionRoot: Buffer.from('b27ca21f40d44113c2090ca8f05fb706c54e87dd', 'hex'),
58+
assetRoot: Buffer.from('b27ca21f40d44113c2090ca8f05fb706c54e87dd', 'hex'),
59+
eventRoot: Buffer.from(
60+
'30dda4fbc395828e5a9f2f8824771e434fce4945a1e7820012440d09dd1e2b6d',
61+
'hex',
62+
),
63+
generatorAddress: Buffer.from('be63fb1c0426573352556f18b21efd5b6183c39c', 'hex'),
64+
maxHeightPrevoted: 1000988,
65+
maxHeightGenerated: 1000988,
66+
impliesMaxPrevotes: true,
67+
validatorsHash: utils.hash(Buffer.alloc(0)),
68+
aggregateCommit: {
69+
height: 0,
70+
aggregationBits: Buffer.alloc(0),
71+
certificateSignature: Buffer.alloc(0),
72+
},
73+
signature: Buffer.from('6da88e2fd4435e26e02682435f108002ccc3ddd5', 'hex'),
74+
...attrs,
75+
});
76+
const blockHeader = new BlockHeader(getBlockAttrs());
77+
const block = new Block(blockHeader, [transaction], blockAsset);
78+
const validBlockID = '215b667a32a5cd51a94c9c2046c11fffb08c65748febec099451e3b164452b';
3379

3480
beforeEach(() => {
3581
stateStore = new StateStore(new InMemoryDatabase());
3682
endpoint = new ChainEndpoint({
3783
chain: {
3884
dataAccess: {
3985
getEvents: jest.fn(),
86+
getBlockByID: jest.fn(),
87+
getBlockByHeight: jest.fn(),
88+
getBlocksByHeightBetween: jest.fn(),
89+
getTransactionByID: jest.fn(),
4090
},
91+
lastBlock: block,
4192
} as any,
4293
bftMethod: {
4394
getSlotNumber: jest.fn().mockReturnValue(0),
@@ -229,4 +280,254 @@ describe('Chain endpoint', () => {
229280
expect(list[1].nextAllocatedTime - list[0].nextAllocatedTime).toBe(DEFAULT_INTERVAL);
230281
});
231282
});
283+
284+
describe('getBlockByID', () => {
285+
it('should throw if provided block id is not valid', async () => {
286+
await expect(
287+
endpoint.getBlockByID(createRequestContext({ id: 'invalid id' })),
288+
).rejects.toThrow('Invalid parameters. id must be a valid hex string.');
289+
});
290+
291+
it('should return the block if provided id is valid', async () => {
292+
jest.spyOn(endpoint['_chain'].dataAccess, 'getBlockByID').mockResolvedValue(block);
293+
await expect(
294+
endpoint.getBlockByID(
295+
createRequestContext({
296+
id: validBlockID,
297+
}),
298+
),
299+
).resolves.toEqual(block.toJSON());
300+
});
301+
});
302+
303+
describe('getBlocksByIDs', () => {
304+
it('should throw if the provided block ids is an empty array or not a valid array', async () => {
305+
await expect(endpoint.getBlocksByIDs(createRequestContext({ ids: [] }))).rejects.toThrow(
306+
'Invalid parameters. ids must be a non empty array.',
307+
);
308+
309+
await expect(
310+
endpoint.getBlocksByIDs(createRequestContext({ ids: 'not an array' })),
311+
).rejects.toThrow('Invalid parameters. ids must be a non empty array.');
312+
});
313+
314+
it('should throw if any of the provided block ids is not valid', async () => {
315+
await expect(
316+
endpoint.getBlocksByIDs(createRequestContext({ ids: [validBlockID, 'invalid id'] })),
317+
).rejects.toThrow('Invalid parameters. id must a valid hex string.');
318+
});
319+
320+
it('should return empty result if the provided block ids are not found', async () => {
321+
jest.spyOn(endpoint['_chain'].dataAccess, 'getBlockByID').mockImplementation(() => {
322+
throw new NotFoundError();
323+
});
324+
325+
await expect(
326+
endpoint.getBlocksByIDs(createRequestContext({ ids: [validBlockID] })),
327+
).resolves.toEqual([]);
328+
});
329+
330+
it('should throw if dataAccess throws an error other than NotFoundError', async () => {
331+
jest.spyOn(endpoint['_chain'].dataAccess, 'getBlockByID').mockImplementation(() => {
332+
throw new Error();
333+
});
334+
335+
await expect(
336+
endpoint.getBlocksByIDs(createRequestContext({ ids: [validBlockID] })),
337+
).rejects.toThrow();
338+
});
339+
340+
it('should return a collection of blocks', async () => {
341+
jest.spyOn(endpoint['_chain'].dataAccess, 'getBlockByID').mockResolvedValue(block);
342+
343+
await expect(
344+
endpoint.getBlocksByIDs(createRequestContext({ ids: [validBlockID] })),
345+
).resolves.toEqual([block.toJSON()]);
346+
});
347+
});
348+
349+
describe('getBlockByHeight', () => {
350+
it('should throw if provided height is invalid', async () => {
351+
await expect(
352+
endpoint.getBlockByHeight(createRequestContext({ height: 'incorrect height' })),
353+
).rejects.toThrow('Invalid parameters. height must be a number.');
354+
});
355+
356+
it('should rerturn a block if the provided height is valid', async () => {
357+
jest.spyOn(endpoint['_chain'].dataAccess, 'getBlockByHeight').mockResolvedValue(block);
358+
359+
await expect(endpoint.getBlockByHeight(createRequestContext({ height: 1 }))).resolves.toEqual(
360+
block.toJSON(),
361+
);
362+
});
363+
});
364+
365+
describe('getBlocksByHeightBetween', () => {
366+
it('should throw if provided heights are invalid', async () => {
367+
await expect(
368+
endpoint.getBlocksByHeightBetween(
369+
createRequestContext({ from: 'incorrect height', to: 10 }),
370+
),
371+
).rejects.toThrow('Invalid parameters. from and to must be a number.');
372+
373+
await expect(
374+
endpoint.getBlocksByHeightBetween(
375+
createRequestContext({ from: 1, to: 'incorrect height' }),
376+
),
377+
).rejects.toThrow('Invalid parameters. from and to must be a number.');
378+
});
379+
380+
it('should return a collection of blocks', async () => {
381+
jest
382+
.spyOn(endpoint['_chain'].dataAccess, 'getBlocksByHeightBetween')
383+
.mockResolvedValue([block]);
384+
385+
await expect(
386+
endpoint.getBlocksByHeightBetween(createRequestContext({ from: 1, to: 10 })),
387+
).resolves.toEqual([block.toJSON()]);
388+
});
389+
});
390+
391+
describe('getLastBlock', () => {
392+
it('should return the last block', () => {
393+
expect(endpoint.getLastBlock()).toEqual(block.toJSON());
394+
});
395+
});
396+
397+
describe('getTransactionByID', () => {
398+
it('should throw if provided id is not valid', async () => {
399+
await expect(
400+
endpoint.getTransactionByID(createRequestContext({ id: 'invalid id' })),
401+
).rejects.toThrow('Invalid parameters. id must be a valid hex string.');
402+
});
403+
404+
it('should return a transaction if provided id is valid', async () => {
405+
jest
406+
.spyOn(endpoint['_chain'].dataAccess, 'getTransactionByID')
407+
.mockResolvedValue(transaction);
408+
409+
await expect(
410+
endpoint.getTransactionByID(createRequestContext({ id: transaction.id.toString('hex') })),
411+
).resolves.toEqual(transaction.toJSON());
412+
});
413+
});
414+
415+
describe('getTransactionsByIDs', () => {
416+
it('should throw if provided ids is empty or not an array', async () => {
417+
await expect(
418+
endpoint.getTransactionsByIDs(createRequestContext({ ids: [] })),
419+
).rejects.toThrow('Invalid parameters. ids must be a non empty array');
420+
421+
await expect(
422+
endpoint.getTransactionsByIDs(createRequestContext({ ids: 'invalid id' })),
423+
).rejects.toThrow('Invalid parameters. ids must be a non empty array');
424+
});
425+
426+
it('should throw if any of the provided ids are not valid', async () => {
427+
await expect(
428+
endpoint.getTransactionsByIDs(createRequestContext({ ids: [validBlockID, 'invalid ID'] })),
429+
).rejects.toThrow('Invalid parameters. id must be a valid hex string.');
430+
});
431+
432+
it('should return a collection of transactions', async () => {
433+
jest
434+
.spyOn(endpoint['_chain'].dataAccess, 'getTransactionByID')
435+
.mockResolvedValue(transaction);
436+
437+
await expect(
438+
endpoint.getTransactionsByIDs(
439+
createRequestContext({ ids: [transaction.id.toString('hex')] }),
440+
),
441+
).resolves.toEqual([transaction.toJSON()]);
442+
});
443+
});
444+
445+
describe('getTransactionsByHeight', () => {
446+
it('should throw if provided height is invalid', async () => {
447+
await expect(
448+
endpoint.getTransactionsByHeight(createRequestContext({ height: 'invalid height' })),
449+
).rejects.toThrow('Invalid parameters. height must be zero or a positive number.');
450+
451+
await expect(
452+
endpoint.getTransactionsByHeight(createRequestContext({ height: -1 })),
453+
).rejects.toThrow('Invalid parameters. height must be zero or a positive number.');
454+
});
455+
456+
it('should return a collection of transactions in the block at the provided height', async () => {
457+
jest.spyOn(endpoint['_chain'].dataAccess, 'getBlockByHeight').mockResolvedValue(block);
458+
459+
await expect(
460+
endpoint.getTransactionsByHeight(createRequestContext({ height: 1 })),
461+
).resolves.toEqual(block.transactions.map(t => t.toJSON()));
462+
});
463+
});
464+
465+
describe('getAssetsByHeight', () => {
466+
it('should throw if provided height is invalid', async () => {
467+
await expect(
468+
endpoint.getAssetsByHeight(createRequestContext({ height: 'invalid height' })),
469+
).rejects.toThrow('Invalid parameters. height must be zero or a positive number.');
470+
471+
await expect(
472+
endpoint.getAssetsByHeight(createRequestContext({ height: -1 })),
473+
).rejects.toThrow('Invalid parameters. height must be zero or a positive number.');
474+
});
475+
476+
it('should return block assests at the provided height', async () => {
477+
jest.spyOn(endpoint['_chain'].dataAccess, 'getBlockByHeight').mockResolvedValue(block);
478+
479+
await expect(
480+
endpoint.getAssetsByHeight(createRequestContext({ height: 1 })),
481+
).resolves.toEqual(block.assets.toJSON());
482+
});
483+
});
484+
485+
describe('areHeadersContradicting', () => {
486+
it('should throw if provided parameters are not valid', async () => {
487+
await expect(
488+
endpoint.areHeadersContradicting(
489+
createRequestContext({
490+
header1: 'header1',
491+
header2: blockHeader.getBytes().toString('hex'),
492+
}),
493+
),
494+
).rejects.toThrow(`'.header1' must match format "hex"`);
495+
496+
await expect(
497+
endpoint.areHeadersContradicting(
498+
createRequestContext({
499+
header1: block.getBytes().toString('hex'),
500+
header2: blockHeader.getBytes().toString(),
501+
}),
502+
),
503+
).rejects.toThrow(`'.header2' must match format "hex"`);
504+
});
505+
506+
it('should invalidate if both headers have same id', async () => {
507+
await expect(
508+
endpoint.areHeadersContradicting(
509+
createRequestContext({
510+
header1: blockHeader.getBytes().toString('hex'),
511+
header2: blockHeader.getBytes().toString('hex'),
512+
}),
513+
),
514+
).resolves.toEqual({ valid: false });
515+
});
516+
517+
it('should invoke areDistinctHeadersContradicting for the provided headers', async () => {
518+
const contradictingBlockHeader = new BlockHeader(getBlockAttrs({ version: 2 }));
519+
520+
jest.spyOn(bftUtils, 'areDistinctHeadersContradicting').mockReturnValue(false);
521+
await expect(
522+
endpoint.areHeadersContradicting(
523+
createRequestContext({
524+
header1: blockHeader.getBytes().toString('hex'),
525+
header2: contradictingBlockHeader.getBytes().toString('hex'),
526+
}),
527+
),
528+
).resolves.toEqual({ valid: false });
529+
530+
expect(bftUtils.areDistinctHeadersContradicting).toHaveBeenCalledTimes(1);
531+
});
532+
});
232533
});

0 commit comments

Comments
 (0)