diff --git a/asset-transfer-basic/rest-api-typescript/package-lock.json b/asset-transfer-basic/rest-api-typescript/package-lock.json index 3c44608f..99399c39 100644 --- a/asset-transfer-basic/rest-api-typescript/package-lock.json +++ b/asset-transfer-basic/rest-api-typescript/package-lock.json @@ -1215,9 +1215,9 @@ } }, "@types/ioredis": { - "version": "4.26.4", - "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.26.4.tgz", - "integrity": "sha512-QFbjNq7EnOGw6d1gZZt2h26OFXjx7z+eqEnbCHSrDI1OOLEgOHMKdtIajJbuCr9uO+X9kQQRe7Lz6uxqxl5XKg==", + "version": "4.26.6", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.26.6.tgz", + "integrity": "sha512-Q9ydXL/5Mot751i7WLCm9OGTj5jlW3XBdkdEW21SkXZ8Y03srbkluFGbM3q8c+vzPW30JOLJ+NsZWHoly0+13A==", "dev": true, "requires": { "@types/node": "*" diff --git a/asset-transfer-basic/rest-api-typescript/src/__mocks__/IORedis.ts b/asset-transfer-basic/rest-api-typescript/src/__mocks__/IORedis.ts new file mode 100644 index 00000000..bc31167e --- /dev/null +++ b/asset-transfer-basic/rest-api-typescript/src/__mocks__/IORedis.ts @@ -0,0 +1,21 @@ +import { RedisOptions } from 'ioredis'; + +class IORedis { + redisOptions: RedisOptions; + constructor(options: RedisOptions) { + this.redisOptions = options; + } + + hincrby = jest.fn().mockReturnThis(); + multi = jest.fn().mockReturnThis(); + del = jest.fn().mockReturnThis(); + + zrem = jest.fn().mockReturnThis(); + + exec = jest.fn().mockReturnThis(); + + hset = jest.fn().mockReturnThis(); + zadd = jest.fn().mockReturnThis(); +} + +export default IORedis; diff --git a/asset-transfer-basic/rest-api-typescript/src/__tests__/fabric.test.ts b/asset-transfer-basic/rest-api-typescript/src/__tests__/fabric.test.ts deleted file mode 100644 index ec2948b9..00000000 --- a/asset-transfer-basic/rest-api-typescript/src/__tests__/fabric.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { retryTransaction } from '../fabric'; - -import { getMockedNetwork } from '../__mocks__/fabric-network'; -import { Redis } from 'ioredis'; -import * as redis from '../redis'; -// import { Gateway, Gateway, Gateway } from 'fabric-network'; - -/** - * retryTransaction - */ -jest.mock('../config'); - -describe('Testing retryTransaction', () => { - let contract: any = null; - const transaction = { - submit: jest.fn().mockRejectedValue({}), - }; - const mockedContact = { - deserializeTransaction: jest.fn().mockReturnValue(transaction), - }; - beforeAll(async () => { - const rejectableGetContract = jest - .fn() - .mockImplementation(() => mockedContact); - - const network = getMockedNetwork(rejectableGetContract)(''); - contract = (await network).getContract(''); - }); - - describe('Check retry increment ', () => { - const transactionId = - '0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95'; - const key = `txn:${transactionId}`; - const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`; - const args = '["test111","red",400,"Jean",101]'; - const timestamp = 1628078044362; - const savedTransaction = { - timestamp: timestamp.toString(), - state: state, - retries: '', - args: args, - }; - - let data: Record = {}; - beforeEach(() => { - data = {}; - const clearTransactionDetails = jest.spyOn( - redis, - 'clearTransactionDetails' - ); - clearTransactionDetails.mockImplementation( - async (redis: Redis, transactionId: string) => { - const key = `txn:${transactionId}`; - delete data[key]; - } - ); - - const incrementRetryCount = jest.spyOn(redis, 'incrementRetryCount'); - incrementRetryCount.mockImplementation( - async (redis: Redis, transactionId: string) => { - const key = `txn:${transactionId}`; - data[key].retries = (parseInt(data[key].retries) + 1).toString(); - } - ); - }); - it('retry count should incremnt to 4', async () => { - savedTransaction.retries = '3'; - data = { [key]: savedTransaction }; - await retryTransaction( - contract, - redis.redis, - transactionId, - savedTransaction - ); - expect(data[key]).toMatchObject({ - timestamp: timestamp.toString(), - state: state, - retries: '4', - args: args, - }); - }); - }); -}); diff --git a/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts b/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts new file mode 100644 index 00000000..3cc7b1d8 --- /dev/null +++ b/asset-transfer-basic/rest-api-typescript/src/fabric.spec.ts @@ -0,0 +1,94 @@ +import { retryTransaction, getGateway } from './fabric'; +import { getMockedNetwork } from './__mocks__/fabric-network'; +import * as config from './config'; + +import IORedis from './__mocks__/IORedis'; +import { Redis } from 'ioredis'; +import { Contract } from 'fabric-network'; + +jest.mock('./config'); +jest.mock('ioredis'); +const redisOptions = { + port: config.redisPort, + host: config.redisHost, + username: config.redisUsername, + password: config.redisPassword, +}; + +const redis = new IORedis(redisOptions) as unknown as Redis; + +describe('Testing retryTransaction', () => { + const transactionId = + '0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95'; + const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`; + const args = '["test111","red",400,"Jean",101]'; + const timestamp = 1628078044362; + const savedTransaction = { + timestamp: timestamp.toString(), + state: state, + retries: '', + args: args, + }; + + describe('Check retry increment ', () => { + const transactionId = + '0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95'; + const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`; + const args = '["test111","red",400,"Jean",101]'; + const timestamp = 1628078044362; + const savedTransaction = { + timestamp: timestamp.toString(), + state: state, + retries: '', + args: args, + }; + + it('Transaction failure, check redis increment func call', async () => { + jest.doMock('fabric-network'); + const transaction = { + submit: jest.fn().mockRejectedValue({}), + }; + const mockedContact = { + deserializeTransaction: jest.fn().mockReturnValue(transaction), + }; + const rejectableGetContract = jest + .fn() + .mockImplementation(() => mockedContact); + + const network = getMockedNetwork(rejectableGetContract)(''); + const contract: Contract = (await network).getContract(''); + savedTransaction.retries = '3'; + await retryTransaction(contract, redis, transactionId, savedTransaction); + expect(redis.hincrby).toHaveBeenCalledTimes(1); + }); + }); + + describe('Transaction successful, check redis delete key func call ', () => { + it('call redis increment', async () => { + jest.doMock('fabric-network'); + const transaction = { + submit: jest.fn().mockResolvedValue({}), + }; + const mockedContact = { + deserializeTransaction: jest.fn().mockReturnValue(transaction), + }; + const resolvableGetContract = jest + .fn() + .mockImplementation(() => mockedContact); + + const network = getMockedNetwork(resolvableGetContract)(''); + const contract: Contract = (await network).getContract(''); + savedTransaction.retries = '3'; + await retryTransaction(contract, redis, transactionId, savedTransaction); + expect(redis.del).toHaveBeenCalledTimes(1); + }); + }); +}); + +describe('Test getGateway', () => { + it('should throw error for invalid org name', async () => { + expect(async () => await getGateway('')).rejects.toThrow( + 'Invalid org name for gateway' + ); + }); +}); diff --git a/asset-transfer-basic/rest-api-typescript/src/fabric.ts b/asset-transfer-basic/rest-api-typescript/src/fabric.ts index b7bee9a3..8af4fefe 100644 --- a/asset-transfer-basic/rest-api-typescript/src/fabric.ts +++ b/asset-transfer-basic/rest-api-typescript/src/fabric.ts @@ -67,6 +67,9 @@ const FabricDataMapper: { [key: string]: FabricConfigType } = { export const getGateway = async (org: string): Promise => { const fabricConfig = FabricDataMapper[org]; + if (fabricConfig == undefined) { + throw new Error('Invalid org name for gateway'); + } logger.debug('Configuring fabric gateway for %s', org); const wallet = await Wallets.newInMemoryWallet(); @@ -78,8 +81,8 @@ export const getGateway = async (org: string): Promise => { mspId: fabricConfig.mspId, type: 'X.509', }; - await wallet.put(fabricConfig.identityName, x509Identity); + await wallet.put(fabricConfig.identityName, x509Identity); const gateway = new Gateway(); const connectOptions: GatewayOptions = { diff --git a/asset-transfer-basic/rest-api-typescript/src/redis.spec.ts b/asset-transfer-basic/rest-api-typescript/src/redis.spec.ts new file mode 100644 index 00000000..7d81cb99 --- /dev/null +++ b/asset-transfer-basic/rest-api-typescript/src/redis.spec.ts @@ -0,0 +1,75 @@ +import IORedis from './__mocks__/IORedis'; +import * as config from './config'; +import { Redis } from 'ioredis'; +import { + clearTransactionDetails, + incrementRetryCount, + storeTransactionDetails, +} from './redis'; + +jest.mock('ioredis'); +jest.mock('./config'); + +const redisOptions = { + port: config.redisPort, + host: config.redisHost, + username: config.redisUsername, + password: config.redisPassword, +}; +const redis = new IORedis(redisOptions) as unknown as Redis; +describe('Testing increment retries ', () => { + const transactionId = + '0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95'; + it('Should increment retries for valid transction id', async () => { + await incrementRetryCount(redis, transactionId); + expect(redis.hincrby).toHaveBeenCalledTimes(1); + }); + + it('Should not increment retries for empty transaction id ', async () => { + await incrementRetryCount(redis, ''); + expect(redis.hincrby).toHaveBeenCalledTimes(0); + }); +}); + +describe('Testing storeTransactionDetails ', () => { + const args = '["test111","red",400,"Jean",101]'; + const timestamp = 1628078044362; + it('Should store details for valid transction Id', async () => { + const transactionId = + '0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95'; + const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`; + await storeTransactionDetails( + redis, + transactionId, + Buffer.from(state), + args, + timestamp + ); + expect(redis.hset).toHaveBeenCalledTimes(1); + expect(redis.zadd).toHaveBeenCalledTimes(1); + }); + + it('Should not store details for empty transction Id', async () => { + const transactionId = ''; + const state = `{"name":"CreateAsset","nonce":"damqinq8nrI4n4qY8lFVsZw7RwG2ufrv","transactionId":${transactionId}`; + await storeTransactionDetails( + redis, + transactionId, + Buffer.from(state), + args, + timestamp + ); + expect(redis.hset).toHaveBeenCalledTimes(0); + expect(redis.zadd).toHaveBeenCalledTimes(0); + }); +}); + +describe('Testing clearTransactionDetails ', () => { + it('Should clear details ', async () => { + const transactionId = + '0ae62c01e4c4b112c3f3954a2f11243da76778e46df9ad2783bcbafc79652b95'; + await clearTransactionDetails(redis, transactionId); + expect(redis.del).toHaveBeenCalledTimes(1); + expect(redis.zrem).toHaveBeenCalledTimes(1); + }); +}); diff --git a/asset-transfer-basic/rest-api-typescript/src/redis.ts b/asset-transfer-basic/rest-api-typescript/src/redis.ts index 2077a68b..c4e8b7c3 100644 --- a/asset-transfer-basic/rest-api-typescript/src/redis.ts +++ b/asset-transfer-basic/rest-api-typescript/src/redis.ts @@ -23,29 +23,40 @@ export const storeTransactionDetails = async ( transactionArgs: string, timestamp: number ): Promise => { - const key = `txn:${transactionId}`; - logger.debug( - 'Storing transaction details. Key: %s State: %s Args: %s Timestamp: %d', - key, - transactionState, - transactionArgs, - timestamp - ); - await redis - .multi() - .hset( + try { + if (transactionId.length === 0) { + throw new Error('Empty transaction Id found'); + } + const key = `txn:${transactionId}`; + logger.debug( + 'Storing transaction details. Key: %s State: %s Args: %s Timestamp: %d', key, - 'state', transactionState, - 'args', transactionArgs, - 'timestamp', - timestamp, - 'retries', - '0' - ) - .zadd('index:txn:timestamp', timestamp, transactionId) - .exec(); + timestamp + ); + await redis + .multi() + .hset( + key, + 'state', + transactionState, + 'args', + transactionArgs, + 'timestamp', + timestamp, + 'retries', + '0' + ) + .zadd('index:txn:timestamp', timestamp, transactionId) + .exec(); + } catch (err) { + logger.error( + err, + 'Error storing transaction details. ID %s', + transactionId + ); + } }; export const clearTransactionDetails = async ( @@ -78,6 +89,9 @@ export const incrementRetryCount = async ( const key = `txn:${transactionId}`; logger.debug('Incrementing retries fortransaction Key: %s', key); try { + if (transactionId.length === 0) { + throw new Error('Empty transaction Id found'); + } await (redis as Redis).hincrby(`txn:${transactionId}`, 'retries', 1); } catch (err) { logger.error(