From 862080773e8a87ffbd686f0136298645e88c6d15 Mon Sep 17 00:00:00 2001 From: James Taylor Date: Mon, 2 Aug 2021 14:09:11 +0100 Subject: [PATCH] Initial API tests Signed-off-by: James Taylor --- .../rest-api-typescript/package-lock.json | 73 +++ .../rest-api-typescript/package.json | 2 + .../src/__mocks__/fabric-network.ts | 171 ++++- .../src/__tests__/api.test.ts | 592 +++++++++++++++++- .../rest-api-typescript/src/assets.router.ts | 5 +- .../rest-api-typescript/src/fabric.ts | 8 +- 6 files changed, 826 insertions(+), 25 deletions(-) diff --git a/asset-transfer-basic/rest-api-typescript/package-lock.json b/asset-transfer-basic/rest-api-typescript/package-lock.json index 99399c39..e7e35369 100644 --- a/asset-transfer-basic/rest-api-typescript/package-lock.json +++ b/asset-transfer-basic/rest-api-typescript/package-lock.json @@ -2934,6 +2934,31 @@ "bser": "2.1.1" } }, + "fengari": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/fengari/-/fengari-0.1.4.tgz", + "integrity": "sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g==", + "dev": true, + "requires": { + "readline-sync": "^1.4.9", + "sprintf-js": "^1.1.1", + "tmp": "^0.0.33" + }, + "dependencies": { + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + } + } + }, + "fengari-interop": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/fengari-interop/-/fengari-interop-0.1.2.tgz", + "integrity": "sha512-8iTvaByZVoi+lQJhHH9vC+c/Yaok9CwOqNQZN6JrVpjmWwW4dDkeblBXhnHC+BoI6eF4Cy5NKW3z6ICEjvgywQ==", + "dev": true + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3366,6 +3391,18 @@ } } }, + "ioredis-mock": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/ioredis-mock/-/ioredis-mock-5.6.0.tgz", + "integrity": "sha512-Ow+tyKdijg/gA2gSEv7lq8dLp6bO7FnwDXbJ9as37NF23XNRGMLzBc7ITaqMydfrbTodWnLcE2lKEaBs7SBpyA==", + "dev": true, + "requires": { + "fengari": "^0.1.4", + "fengari-interop": "^0.1.2", + "lodash": "^4.17.21", + "standard-as-callback": "^2.1.0" + } + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4299,6 +4336,15 @@ } } }, + "jest-mock-extended": { + "version": "2.0.2-beta2", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-2.0.2-beta2.tgz", + "integrity": "sha512-56zcpgRPs3YxQP0ejcaaNFxUinPyRxQCbuk7GGORZqEbAFuQVXWAAtru2tI1N4qcLBoDWEJ/hwUxwbEGY5hdyw==", + "dev": true, + "requires": { + "ts-essentials": "^7.0.3" + } + }, "jest-pnp-resolver": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", @@ -5246,6 +5292,12 @@ "word-wrap": "^1.2.3" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-each-series": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", @@ -5636,6 +5688,12 @@ "util-deprecate": "^1.0.1" } }, + "readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "dev": true + }, "redis-commands": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", @@ -6198,6 +6256,15 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -6258,6 +6325,12 @@ } } }, + "ts-essentials": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "dev": true + }, "ts-jest": { "version": "27.0.4", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.4.tgz", diff --git a/asset-transfer-basic/rest-api-typescript/package.json b/asset-transfer-basic/rest-api-typescript/package.json index 2f1e86fc..18919584 100644 --- a/asset-transfer-basic/rest-api-typescript/package.json +++ b/asset-transfer-basic/rest-api-typescript/package.json @@ -32,7 +32,9 @@ "eslint": "^7.29.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^3.4.0", + "ioredis-mock": "^5.6.0", "jest": "^27.0.6", + "jest-mock-extended": "^2.0.2-beta2", "pino-pretty": "^5.0.2", "prettier": "^2.3.1", "rimraf": "^3.0.2", diff --git a/asset-transfer-basic/rest-api-typescript/src/__mocks__/fabric-network.ts b/asset-transfer-basic/rest-api-typescript/src/__mocks__/fabric-network.ts index 9ae7e06e..d13ff057 100644 --- a/asset-transfer-basic/rest-api-typescript/src/__mocks__/fabric-network.ts +++ b/asset-transfer-basic/rest-api-typescript/src/__mocks__/fabric-network.ts @@ -2,7 +2,47 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { mock } from 'jest-mock-extended'; +import { Contract, Network, Transaction, WalletStore } from 'fabric-network'; import { mocked } from 'ts-jest/utils'; +import * as fabricProtos from 'fabric-protos'; + +const mockAsset1 = { + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, +}; +const mockAsset1Buffer = Buffer.from(JSON.stringify(mockAsset1)); + +const mockAsset2 = { + ID: 'asset2', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, +}; + +const mockAllAssetsBuffer = Buffer.from( + JSON.stringify([mockAsset1, mockAsset2]) +); + +const mockBlockchainInfoProto = fabricProtos.common.BlockchainInfo.create(); +mockBlockchainInfoProto.height = 42; +const mockBlockchainInfoBuffer = Buffer.from( + fabricProtos.common.BlockchainInfo.encode(mockBlockchainInfoProto).finish() +); + +const processedTransactionProto = + fabricProtos.protos.ProcessedTransaction.create(); +processedTransactionProto.validationCode = + fabricProtos.protos.TxValidationCode.VALID; +const processedTransactionBuffer = Buffer.from( + fabricProtos.protos.ProcessedTransaction.encode( + processedTransactionProto + ).finish() +); type FabricNetworkModule = jest.Mocked; @@ -14,24 +54,124 @@ const { Wallets, }: FabricNetworkModule = jest.createMockFromModule('fabric-network'); +const mockWalletStore = mock(); mocked(Wallets.newInMemoryWallet).mockResolvedValue( - new Wallet({ - get: jest.fn(), - list: jest.fn(), - put: jest.fn(), - remove: jest.fn(), - }) + new Wallet(mockWalletStore) ); -mocked(Gateway.prototype.getNetwork).mockResolvedValue({ - getGateway: jest.fn(), - getContract: jest.fn(), - getChannel: jest.fn(), - addCommitListener: jest.fn(), - removeCommitListener: jest.fn(), - addBlockListener: jest.fn(), - removeBlockListener: jest.fn(), -}); +const mockAssetExistsTransaction = mock(); +mockAssetExistsTransaction.evaluate + .calledWith('asset1') + .mockResolvedValue(Buffer.from('true')); +mockAssetExistsTransaction.evaluate + .calledWith('asset3') + .mockResolvedValue(Buffer.from('false')); + +const mockReadAssetTransaction = mock(); +mockReadAssetTransaction.evaluate + .calledWith('asset1') + .mockResolvedValue(mockAsset1Buffer); +mockReadAssetTransaction.evaluate + .calledWith('asset3') + .mockRejectedValue(new Error('the asset asset3 does not exist')); + +const mockCreateAssetTransaction = mock(); +mockCreateAssetTransaction.getTransactionId.mockReturnValue('txn1'); +mockCreateAssetTransaction.submit + .calledWith('asset1') + .mockRejectedValue( + new Error( + 'No valid responses from any peers. Errors:\n peer=peer0.org1.example.com:7051, status=500, message=the asset asset1 already exists\n peer=peer0.org2.example.com:9051, status=500, message=the asset asset3 already exists' + ) + ); + +// NOTE: only the second mocked GetAllAssets with return no assets +// TODO find a better alternative so that test order does not matter +const mockGetAllAssetsTransaction = mock(); +mockGetAllAssetsTransaction.evaluate + .mockResolvedValueOnce(Buffer.from('')) + .mockResolvedValueOnce(mockAllAssetsBuffer); + +const mockUpdateAssetTransaction = mock(); +mockUpdateAssetTransaction.getTransactionId.mockReturnValue('txn1'); +mockUpdateAssetTransaction.submit + .calledWith('asset3') + .mockRejectedValue( + new Error( + 'No valid responses from any peers. Errors:\n peer=peer0.org1.example.com:7051, status=500, message=the asset asset3 does not exist\n peer=peer0.org2.example.com:9051, status=500, message=the asset asset3 does not exist' + ) + ); + +const mockTransferAssetTransaction = mock(); +mockTransferAssetTransaction.getTransactionId.mockReturnValue('txn1'); +mockTransferAssetTransaction.submit + .calledWith('asset3') + .mockRejectedValue( + new Error( + 'No valid responses from any peers. Errors:\n peer=peer0.org1.example.com:7051, status=500, message=the asset asset3 does not exist\n peer=peer0.org2.example.com:9051, status=500, message=the asset asset3 does not exist' + ) + ); + +const mockDeleteAssetTransaction = mock(); +mockDeleteAssetTransaction.getTransactionId.mockReturnValue('txn1'); +mockDeleteAssetTransaction.submit + .calledWith('asset3') + .mockRejectedValue( + new Error( + 'No valid responses from any peers. Errors:\n peer=peer0.org1.example.com:7051, status=500, message=the asset asset3 does not exist\n peer=peer0.org2.example.com:9051, status=500, message=the asset asset3 does not exist' + ) + ); + +const mockBasicContract = mock(); +mockBasicContract.createTransaction + .calledWith('AssetExists') + .mockReturnValue(mockAssetExistsTransaction); +mockBasicContract.createTransaction + .calledWith('ReadAsset') + .mockReturnValue(mockReadAssetTransaction); +mockBasicContract.createTransaction + .calledWith('CreateAsset') + .mockReturnValue(mockCreateAssetTransaction); +mockBasicContract.createTransaction + .calledWith('GetAllAssets') + .mockReturnValue(mockGetAllAssetsTransaction); +mockBasicContract.createTransaction + .calledWith('UpdateAsset') + .mockReturnValue(mockUpdateAssetTransaction); +mockBasicContract.createTransaction + .calledWith('TransferAsset') + .mockReturnValue(mockTransferAssetTransaction); +mockBasicContract.createTransaction + .calledWith('DeleteAsset') + .mockReturnValue(mockDeleteAssetTransaction); + +const mockGetTransactionByIDTransaction = mock(); +mockGetTransactionByIDTransaction.evaluate + .calledWith('mychannel', 'txn1') + .mockResolvedValue(processedTransactionBuffer); +mockGetTransactionByIDTransaction.evaluate + .calledWith('mychannel', 'txn3') + .mockRejectedValue( + new Error( + 'Failed to get transaction with id txn3, error Entry not found in index' + ) + ); + +const mockSystemContract = mock(); +mockSystemContract.evaluateTransaction + .calledWith('GetChainInfo') + .mockResolvedValue(mockBlockchainInfoBuffer); +mockSystemContract.createTransaction + .calledWith('GetTransactionByID') + .mockReturnValue(mockGetTransactionByIDTransaction); + +const mockNetwork = mock(); +mockNetwork.getContract.calledWith('basic').mockReturnValue(mockBasicContract); +mockNetwork.getContract.calledWith('qscc').mockReturnValue(mockSystemContract); + +mocked(Gateway.prototype.getNetwork).mockResolvedValue(mockNetwork); + +// TODO remove this and use simpler mocks in fabric spec tests const getMockedNetwork = (getContract = jest.fn()) => { return mocked(Gateway.prototype.getNetwork).mockResolvedValue({ getGateway: jest.fn(), @@ -47,6 +187,7 @@ const getMockedNetwork = (getContract = jest.fn()) => { export { DefaultEventHandlerStrategies, DefaultQueryHandlerStrategies, + Contract, Gateway, Wallets, getMockedNetwork, diff --git a/asset-transfer-basic/rest-api-typescript/src/__tests__/api.test.ts b/asset-transfer-basic/rest-api-typescript/src/__tests__/api.test.ts index 0da71ca3..9b686f63 100644 --- a/asset-transfer-basic/rest-api-typescript/src/__tests__/api.test.ts +++ b/asset-transfer-basic/rest-api-typescript/src/__tests__/api.test.ts @@ -2,13 +2,15 @@ * SPDX-License-Identifier: Apache-2.0 */ +jest.mock('fabric-network'); +jest.mock('ioredis', () => require('ioredis-mock/jest')); + import { createServer } from '../server'; import { Application } from 'express'; import request from 'supertest'; -jest.mock('fabric-network'); -jest.mock('ioredis'); - +// TODO add tests for server errors +// TODO implement 405 Method Not Allowed where appropriate and add tests describe('Asset Transfer Besic REST API', () => { let app: Application; @@ -16,15 +18,593 @@ describe('Asset Transfer Besic REST API', () => { app = await createServer(); }); - describe('GET /ready', () => { - it('should respond with success json', async () => { + describe('/ready', () => { + it('GET should respond with 200 OK json', async () => { const response = await request(app).get('/ready'); expect(response.statusCode).toEqual(200); expect(response.header).toHaveProperty( 'content-type', 'application/json; charset=utf-8' ); - expect(response.body.status).toEqual('OK'); + expect(response.body).toEqual({ + status: 'OK', + timestamp: expect.any(String), + }); + }); + }); + + describe('/live', () => { + it('GET should respond with 200 OK json', async () => { + const response = await request(app).get('/live'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'OK', + timestamp: expect.any(String), + }); + }); + }); + + describe('/api/assets', () => { + it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .get('/api/assets') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with an empty json array when there are no assets', async () => { + // NOTE: only the first mocked GetAllAssets with return no assets + // TODO find a better alternative so that test order does not matter + const response = await request(app) + .get('/api/assets') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual([]); + }); + + it('GET should respond with json array of assets', async () => { + // NOTE: only the second mocked GetAllAssets with return no assets + // TODO find a better alternative so that test order does not matter + const response = await request(app) + .get('/api/assets') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual([ + { + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }, + { + ID: 'asset2', + Color: 'red', + Size: 5, + Owner: 'Brad', + AppraisedValue: 400, + }, + ]); + }); + + it('POST should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .post('/api/assets') + .send({ + ID: 'asset6', + Color: 'white', + Size: 15, + Owner: 'Michel', + AppraisedValue: 800, + }) + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('POST should respond with 400 bad request json for invalid asset json', async () => { + const response = await request(app) + .post('/api/assets') + .send({ + identifier: 'asset3', + color: 'red', + size: 5, + owner: 'Brad', + appraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(400); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Bad Request', + reason: 'VALIDATION_ERROR', + errors: [ + { + location: 'body', + msg: 'must be a string', + param: 'id', + }, + ], + message: 'Invalid request body', + timestamp: expect.any(String), + }); + }); + + it('POST should respond with 202 accepted json', async () => { + const response = await request(app) + .post('/api/assets') + .send({ + id: 'asset3', + color: 'red', + size: 5, + owner: 'Brad', + appraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(202); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Accepted', + transactionId: 'txn1', + timestamp: expect.any(String), + }); + }); + + it('POST should respond with 409 conflict json when asset already exists', async () => { + const response = await request(app) + .post('/api/assets') + .send({ + id: 'asset1', + color: 'blue', + size: 5, + owner: 'Tomoko', + appraisedValue: 300, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(409); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Conflict', + reason: 'ASSET_EXISTS', + message: 'the asset asset1 already exists', + timestamp: expect.any(String), + }); + }); + }); + + describe('/api/assets/:id', () => { + it('OPTIONS should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .options('/api/assets/asset1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('OPTIONS should respond with 404 not found json without the allow header when there is no asset with the specified ID', async () => { + const response = await request(app) + .options('/api/assets/asset3') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.header).not.toHaveProperty('allow'); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('OPTIONS should respond with 200 OK json with the allow header', async () => { + const response = await request(app) + .options('/api/assets/asset1') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.header).toHaveProperty( + 'allow', + 'DELETE,GET,OPTIONS,PATCH,PUT' + ); + expect(response.body).toEqual({ + status: 'OK', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .get('/api/assets/asset1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with 404 not found json when there is no asset with the specified ID', async () => { + const response = await request(app) + .get('/api/assets/asset3') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with the asset json when the asset exists', async () => { + const response = await request(app) + .get('/api/assets/asset1') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }); + }); + + it('PUT should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .put('/api/assets/asset1') + .send({ + id: 'asset3', + color: 'red', + size: 5, + owner: 'Brad', + appraisedValue: 400, + }) + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('PUT should respond with 404 not found json when there is no asset with the specified ID', async () => { + const response = await request(app) + .put('/api/assets/asset3') + .send({ + id: 'asset3', + color: 'red', + size: 5, + owner: 'Brad', + appraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('PUT should respond with 400 bad request json when IDs do not match', async () => { + const response = await request(app) + .put('/api/assets/asset1') + .send({ + id: 'asset2', + color: 'red', + size: 5, + owner: 'Brad', + appraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(400); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Bad Request', + reason: 'ASSET_ID_MISMATCH', + message: 'Asset IDs must match', + timestamp: expect.any(String), + }); + }); + + it('PUT should respond with 400 bad request json for invalid asset json', async () => { + const response = await request(app) + .put('/api/assets/asset1') + .send({ + identifier: 'asset1', + color: 'red', + size: 5, + owner: 'Brad', + appraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(400); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Bad Request', + reason: 'VALIDATION_ERROR', + errors: [ + { + location: 'body', + msg: 'must be a string', + param: 'id', + }, + ], + message: 'Invalid request body', + timestamp: expect.any(String), + }); + }); + + it('PUT should respond with 202 accepted json', async () => { + const response = await request(app) + .put('/api/assets/asset1') + .send({ + id: 'asset1', + color: 'red', + size: 5, + owner: 'Brad', + appraisedValue: 400, + }) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(202); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Accepted', + transactionId: 'txn1', + timestamp: expect.any(String), + }); + }); + + it('PATCH should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .patch('/api/assets/asset1') + .send([{ op: 'replace', path: '/owner', value: 'Ashleigh' }]) + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('PATCH should respond with 404 not found json when there is no asset with the specified ID', async () => { + const response = await request(app) + .patch('/api/assets/asset3') + .send([{ op: 'replace', path: '/owner', value: 'Ashleigh' }]) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('PATCH should respond with 400 bad request json for invalid patch op/path', async () => { + const response = await request(app) + .patch('/api/assets/asset1') + .send([{ op: 'replace', path: '/color', value: 'orange' }]) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(400); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Bad Request', + reason: 'VALIDATION_ERROR', + errors: [ + { + location: 'body', + msg: "path must be '/owner'", + param: '[0].path', + value: '/color', + }, + ], + message: 'Invalid request body', + timestamp: expect.any(String), + }); + }); + + it('PATCH should respond with 202 accepted json', async () => { + const response = await request(app) + .patch('/api/assets/asset1') + .send([{ op: 'replace', path: '/owner', value: 'Ashleigh' }]) + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(202); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Accepted', + transactionId: 'txn1', + timestamp: expect.any(String), + }); + }); + + it('DELETE should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .delete('/api/assets/asset1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('DELETE should respond with 404 not found json when there is no asset with the specified ID', async () => { + const response = await request(app) + .delete('/api/assets/asset3') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('DELETE should respond with 202 accepted json', async () => { + const response = await request(app) + .delete('/api/assets/asset1') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(202); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Accepted', + transactionId: 'txn1', + timestamp: expect.any(String), + }); + }); + }); + + describe('/api/transactions/:id', () => { + it('GET should respond with 401 unauthorized json when an invalid API key is specified', async () => { + const response = await request(app) + .get('/api/transactions/txn1') + .set('X-Api-Key', 'NOTTHERIGHTAPIKEY'); + expect(response.statusCode).toEqual(401); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + reason: 'NO_VALID_APIKEY', + status: 'Unauthorized', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with 404 not found json when there is no transaction with the specified ID', async () => { + const response = await request(app) + .get('/api/transactions/txn3') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(404); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'Not Found', + timestamp: expect.any(String), + }); + }); + + it('GET should respond with json details for the specified transaction ID', async () => { + const response = await request(app) + .get('/api/transactions/txn1') + .set('X-Api-Key', 'ORG1MOCKAPIKEY'); + expect(response.statusCode).toEqual(200); + expect(response.header).toHaveProperty( + 'content-type', + 'application/json; charset=utf-8' + ); + expect(response.body).toEqual({ + status: 'OK', + progress: 'DONE', + validationCode: 'VALID', + timestamp: expect.any(String), + }); }); }); }); diff --git a/asset-transfer-basic/rest-api-typescript/src/assets.router.ts b/asset-transfer-basic/rest-api-typescript/src/assets.router.ts index b1d89a71..eb3adb58 100644 --- a/asset-transfer-basic/rest-api-typescript/src/assets.router.ts +++ b/asset-transfer-basic/rest-api-typescript/src/assets.router.ts @@ -40,7 +40,10 @@ assetsRouter.get('/', async (req: Request, res: Response) => { try { const contract: Contract = getContractForOrg(req).contract; const data = await evatuateTransaction(contract, 'GetAllAssets'); - const assets = JSON.parse(data.toString()); + let assets = []; + if (data.length > 0) { + assets = JSON.parse(data.toString()); + } return res.status(OK).json(assets); } catch (err) { diff --git a/asset-transfer-basic/rest-api-typescript/src/fabric.ts b/asset-transfer-basic/rest-api-typescript/src/fabric.ts index 8af4fefe..8249c55d 100644 --- a/asset-transfer-basic/rest-api-typescript/src/fabric.ts +++ b/asset-transfer-basic/rest-api-typescript/src/fabric.ts @@ -29,7 +29,7 @@ import { TransactionError, TransactionNotFoundError, } from './errors'; -import fabproto6 from 'fabric-protos'; +import protos from 'fabric-protos'; export const getNetwork = async (gateway: Gateway): Promise => { const network = await gateway.getNetwork(config.channelName); @@ -169,7 +169,9 @@ export const evatuateTransaction = async ( const txnId = txn.getTransactionId(); try { - return await txn.evaluate(...transactionArgs); + const payload = await txn.evaluate(...transactionArgs); + logger.debug({ payload }, 'Evaluate transaction response received'); + return payload; } catch (err) { throw handleError(txnId, err); } @@ -338,7 +340,7 @@ export const getChainInfo = async (qscc: Contract): Promise => { 'GetChainInfo', config.channelName ); - const info = fabproto6.common.BlockchainInfo.decode(data); + const info = protos.common.BlockchainInfo.decode(data); const blockHeight = info.height.toString(); logger.info('Current block height: %s', blockHeight); return true;