From 2241e8bcf479098e1a41d411c887bbb20e2f24e6 Mon Sep 17 00:00:00 2001 From: Roger Hernandez Date: Wed, 17 Jun 2020 14:21:03 +0200 Subject: [PATCH] Revisited transaction endpoints to support groups (confirmed/unconfirmed/partial) --- rest/src/db/CatapultDb.js | 39 +- rest/src/routes/transactionRoutes.js | 80 ++- rest/src/routes/transactionStatusRoutes.js | 4 +- rest/test/db/CatapultDb_spec.js | 76 +- rest/test/routes/allRoutes_spec.js | 10 +- rest/test/routes/transactionRoutes_spec.js | 656 ++++++++++++------ .../routes/transactionStatusRoutes_spec.js | 8 +- 7 files changed, 561 insertions(+), 312 deletions(-) diff --git a/rest/src/db/CatapultDb.js b/rest/src/db/CatapultDb.js index 9ada5cdfd..002520bc7 100644 --- a/rest/src/db/CatapultDb.js +++ b/rest/src/db/CatapultDb.js @@ -99,6 +99,12 @@ const buildBlocksFromOptions = (height, numBlocks, chainHeight) => { const getBoundedPageSize = (pageSize, pagingOptions) => Math.max(pagingOptions.pageSizeMin, Math.min(pagingOptions.pageSizeMax, pageSize || pagingOptions.pageSizeDefault)); +const TransactionGroup = Object.freeze({ + confirmed: 'transactions', + unconfirmed: 'unconfirmedTransactions', + partial: 'partialTransactions' +}); + class CatapultDb { // region construction / connect / disconnect @@ -367,25 +373,16 @@ class CatapultDb { /** * Retrieves filtered and paginated transactions. + * @param {string} group Transactions group on which the query is made. * @param {object} filters Filters to be applied: `address` for an involved address in the query, `signerPublicKey`, `recipientAddress`, - * `group`, `height`, `embedded`, `transactionTypes` array of uint. If `address` is provided, other account related filters are omitted. + * `height`, `embedded`, `transactionTypes` array of uint. If `address` is provided, other account related filters are omitted. * @param {object} options Options for ordering and pagination. Can have an `offset`, and must contain the `sortField`, `sortDirection`, * `pageSize` and `pageNumber`. 'sortField' must be within allowed 'sortingOptions'. * @returns {Promise.} Transactions page. */ - transactions(filters, options) { + transactions(group, filters, options) { const sortingOptions = { id: '_id' }; - const getCollectionName = (transactionStatus = 'confirmed') => { - const collectionNames = { - confirmed: 'transactions', - unconfirmed: 'unconfirmedTransactions', - partial: 'partialTransactions' - }; - return collectionNames[transactionStatus]; - }; - const collectionName = getCollectionName(filters.group); - const buildAccountConditions = () => { if (filters.address) return { 'meta.addresses': Buffer.from(filters.address) }; @@ -433,7 +430,7 @@ class CatapultDb { const sortConditions = { $sort: { [sortingOptions[options.sortField]]: options.sortDirection } }; const conditions = buildConditions(); - return this.queryPagedDocuments_2(conditions, removedFields, sortConditions, collectionName, options); + return this.queryPagedDocuments_2(conditions, removedFields, sortConditions, TransactionGroup[group], options); } transactionsByIdsImpl(collectionName, conditions) { @@ -455,20 +452,12 @@ class CatapultDb { }))); } - transactionsByIds(ids) { - return this.transactionsByIdsImpl('transactions', { _id: { $in: ids.map(id => new ObjectId(id)) } }); - } - - transactionsByHashes(hashes) { - return this.transactionsByIdsImpl('transactions', { 'meta.hash': { $in: hashes.map(hash => Buffer.from(hash)) } }); - } - - transactionsByHashesUnconfirmed(hashes) { - return this.transactionsByIdsImpl('unconfirmedTransactions', { 'meta.hash': { $in: hashes.map(hash => Buffer.from(hash)) } }); + transactionsByIds(group, ids) { + return this.transactionsByIdsImpl(TransactionGroup[group], { _id: { $in: ids.map(id => new ObjectId(id)) } }); } - transactionsByHashesPartial(hashes) { - return this.transactionsByIdsImpl('partialTransactions', { 'meta.hash': { $in: hashes.map(hash => Buffer.from(hash)) } }); + transactionsByHashes(group, hashes) { + return this.transactionsByIdsImpl(TransactionGroup[group], { 'meta.hash': { $in: hashes.map(hash => Buffer.from(hash)) } }); } /** diff --git a/rest/src/routes/transactionRoutes.js b/rest/src/routes/transactionRoutes.js index 4d11ccacd..36dc21f90 100644 --- a/rest/src/routes/transactionRoutes.js +++ b/rest/src/routes/transactionRoutes.js @@ -22,6 +22,8 @@ const routeResultTypes = require('./routeResultTypes'); const routeUtils = require('./routeUtils'); const errors = require('../server/errors'); const catapult = require('catapult-sdk'); +const { NotFoundError } = require('restify-errors'); + const { convert } = catapult.utils; const { PacketType } = catapult.packet; @@ -33,14 +35,7 @@ const constants = { } }; -const parseHeight = params => routeUtils.parseArgument(params, 'height', 'uint'); - -const parseObjectId = str => { - if (!convert.isHexString(str)) - throw Error('must be 12-byte hex string'); - - return str; -}; +const isValidTransactionGroup = group => ['confirmed', 'unconfirmed', 'partial'].includes(group); module.exports = { register: (server, db, services) => { @@ -53,51 +48,66 @@ module.exports = { params => routeUtils.parseArgument(params, 'payload', convert.hexToUint8) ); - routeUtils.addGetPostDocumentRoutes( - server, - sender, - { base: '/transactions', singular: 'transactionId', plural: 'transactionIds' }, - // params has already been converted by a parser below, so it is: string - in case of objectId, Uint8Array - in case of hash - params => (('string' === typeof params[0]) ? db.transactionsByIds(params) : db.transactionsByHashes(params)), - (transactionId, index, array) => { - if (0 < index && array[0].length !== transactionId.length) - throw Error(`all ids must be homogeneous, element ${index}`); - - if (constants.sizes.objectId === transactionId.length) - return parseObjectId(transactionId); - if (constants.sizes.hash === transactionId.length) - return convert.hexToUint8(transactionId); - - throw Error(`invalid length of transaction id '${transactionId}'`); - } - ); - - server.get('/transactions', (req, res, next) => { + server.get('/transactions/:group', (req, res, next) => { const { params } = req; + if (!isValidTransactionGroup(params.group)) + return next(new NotFoundError()); + if (params.address && (params.signerPublicKey || params.recipientAddress)) { throw errors.createInvalidArgumentError( 'can\'t filter by address if signerPublicKey or recipientAddress are already provided' ); } - if (params.group && !['confirmed', 'unconfirmed', 'partial'].includes(params.group)) - throw errors.createInvalidArgumentError('invalid transaction group provided'); - const filters = { - height: params.height ? parseHeight(params) : undefined, + height: params.height ? routeUtils.parseArgument(params, 'height', 'uint') : undefined, address: params.address ? routeUtils.parseArgument(params, 'address', 'address') : undefined, signerPublicKey: params.signerPublicKey ? routeUtils.parseArgument(params, 'signerPublicKey', 'publicKey') : undefined, recipientAddress: params.recipientAddress ? routeUtils.parseArgument(params, 'recipientAddress', 'address') : undefined, transactionTypes: params.type ? routeUtils.parseArgumentAsArray(params, 'type', 'uint') : undefined, - embedded: params.embedded ? routeUtils.parseArgument(params, 'embedded', 'boolean') : undefined, - group: params.group + embedded: params.embedded ? routeUtils.parseArgument(params, 'embedded', 'boolean') : undefined }; const options = routeUtils.parsePaginationArguments(params, services.config.pageSize, { id: 'objectId' }); - return db.transactions(filters, options) + return db.transactions(params.group, filters, options) .then(result => routeUtils.createSender(routeResultTypes.transaction).sendPage(res, next)(result)); }); + + server.get('/transactions/:group/:transactionId', (req, res, next) => { + const { params } = req; + + if (!isValidTransactionGroup(params.group)) + return next(new NotFoundError()); + + let paramType = constants.sizes.objectId === params.transactionId.length ? 'id' : undefined; + paramType = constants.sizes.hash === params.transactionId.length ? 'hash' : paramType; + if (!paramType) + throw Error(`invalid length of transaction id '${params.transactionId}'`); + + const transactionId = routeUtils.parseArgument(params, 'transactionId', 'id' === paramType ? 'objectId' : 'hash256'); + + const dbTransactionsRetriever = 'id' === paramType ? db.transactionsByIds : db.transactionsByHashes; + return dbTransactionsRetriever(params.group, [transactionId]).then(sender.sendOne(params.transactionId, res, next)); + }); + + server.post('/transactions/:group', (req, res, next) => { + const { params } = req; + + if (!isValidTransactionGroup(params.group)) + return next(new NotFoundError()); + + if ((req.params.transactionIds && req.params.hashes) || (!params.transactionIds && !params.hashes)) + throw errors.createInvalidArgumentError('either ids or hashes must be provided'); + + const transactionIds = params.transactionIds + ? routeUtils.parseArgumentAsArray(params, 'transactionIds', 'objectId') + : routeUtils.parseArgumentAsArray(params, 'hashes', 'hash256'); + + const dbTransactionsRetriever = params.transactionIds ? db.transactionsByIds : db.transactionsByHashes; + return dbTransactionsRetriever(params.group, transactionIds) + .then(sender.sendArray(params.transactionIds || params.hashes, res, next)); + }); } }; diff --git a/rest/src/routes/transactionStatusRoutes.js b/rest/src/routes/transactionStatusRoutes.js index 67fdb8f10..1f51e6439 100644 --- a/rest/src/routes/transactionStatusRoutes.js +++ b/rest/src/routes/transactionStatusRoutes.js @@ -31,9 +31,7 @@ module.exports = { routeUtils.addGetPostDocumentRoutes( server, routeUtils.createSender(routeResultTypes.transactionStatus), - { - base: '/transactions', singular: 'hash', plural: 'hashes', postfixes: { singular: 'status', plural: 'statuses' } - }, + { base: '/transactionStatus', singular: 'hash', plural: 'hashes' }, params => dbFacade.transactionStatusesByHashes(db, params, services.config.transactionStates), hash => { if (2 * constants.sizes.hash256 === hash.length) diff --git a/rest/test/db/CatapultDb_spec.js b/rest/test/db/CatapultDb_spec.js index e62202c79..1b7687816 100644 --- a/rest/test/db/CatapultDb_spec.js +++ b/rest/test/db/CatapultDb_spec.js @@ -38,6 +38,12 @@ const DefaultPagingOptions = { pageSizeDefault: 20 }; +const TransactionGroups = { + confirmed: 'confirmed', + unconfirmed: 'unconfirmed', + partial: 'partial' +}; + describe('catapult db', () => { const deleteIds = dbEntities => { test.collection.names.forEach(collectionName => { @@ -823,28 +829,28 @@ describe('catapult db', () => { addTestsWithId(traits, { convertToId: test.db.createObjectId, collectionName: 'transactions', - transactionsByIds: (db, ids) => db.transactionsByIds(ids) + transactionsByIds: (db, ids) => db.transactionsByIds(TransactionGroups.confirmed, ids) })); describe('by transaction hash', () => addTestsWithId(traits, { convertToId: createTransactionHash, collectionName: 'transactions', - transactionsByIds: (db, ids) => db.transactionsByHashes(ids) + transactionsByIds: (db, ids) => db.transactionsByHashes(TransactionGroups.confirmed, ids) })); describe('by transaction hash (unconfirmed)', () => addTestsWithId(traits, { convertToId: createTransactionHash, collectionName: 'unconfirmedTransactions', - transactionsByIds: (db, ids) => db.transactionsByHashesUnconfirmed(ids) + transactionsByIds: (db, ids) => db.transactionsByHashes(TransactionGroups.unconfirmed, ids) })); describe('by transaction hash (partial)', () => addTestsWithId(traits, { convertToId: createTransactionHash, collectionName: 'partialTransactions', - transactionsByIds: (db, ids) => db.transactionsByHashesPartial(ids) + transactionsByIds: (db, ids) => db.transactionsByHashes(TransactionGroups.partial, ids) })); }; @@ -891,11 +897,49 @@ describe('catapult db', () => { // Act + Assert: return runTransactionsDbTest( { transactions: seedTransactions }, - db => db.transactionsByIds([documentId]), + db => db.transactionsByIds(TransactionGroups.confirmed, [documentId]), transactions => assertEqualDocuments([renameId(seedTransactions[4])], transactions) ); }); }); + + describe('translates group to collection name', () => { + const validObjectId = test.db.createObjectId(10); + const validHash = '112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00'; + + const runTransactionsByIdTest = (dbCall, param, group, collection) => { + it(group, () => { + // Arrange: + const transactionsByIdsImplStub = sinon.stub(CatapultDb.prototype, 'transactionsByIdsImpl').returns(''); + const db = new CatapultDb(Object.assign({ networkId: Mijin_Test_Network }, DefaultPagingOptions)); + + // Act + db[dbCall](group, [param]); + + // Assert + expect(transactionsByIdsImplStub.calledOnce).to.equal(true); + expect(transactionsByIdsImplStub.firstCall.args[0]).to.equal(collection); + transactionsByIdsImplStub.restore(); + }); + }; + + const groupToCollectionName = { + confirmed: 'transactions', + unconfirmed: 'unconfirmedTransactions', + partial: 'partialTransactions' + }; + + describe('transactions by ids', () => { + Object.keys(groupToCollectionName).forEach(group => { + runTransactionsByIdTest('transactionsByIds', validObjectId, group, groupToCollectionName[group]); + }); + }); + describe('transactions by hashes', () => { + Object.keys(groupToCollectionName).forEach(group => { + runTransactionsByIdTest('transactionsByHashes', validHash, group, groupToCollectionName[group]); + }); + }); + }); }); describe('names by ids', () => { @@ -1367,7 +1411,7 @@ describe('catapult db', () => { return runDbTest( { transactions: dbTransactions }, - db => db.transactions(filters, options), + db => db.transactions(TransactionGroups.confirmed, filters, options), transactionsPage => { const returnedIds = transactionsPage.data.map(t => t.id); expect(transactionsPage.data.length).to.equal(expectedObjectIds.length); @@ -1385,7 +1429,7 @@ describe('catapult db', () => { // Act + Assert: return runDbTest( { transactions: dbTransactions }, - db => db.transactions({}, paginationOptions), + db => db.transactions(TransactionGroups.confirmed, {}, paginationOptions), page => { const expected_keys = ['meta', 'transaction', 'id']; expect(Object.keys(page.data[0]).sort()).to.deep.equal(expected_keys.sort()); @@ -1402,7 +1446,7 @@ describe('catapult db', () => { // Act + Assert: return runDbTest( { transactions: dbTransactions }, - db => db.transactions({}, paginationOptions), + db => db.transactions(TransactionGroups.confirmed, {}, paginationOptions), transactionsPage => { expect(transactionsPage.data[0].meta.addresses).to.equal(undefined); } @@ -1537,7 +1581,7 @@ describe('catapult db', () => { // Act + Assert: return runDbTest( { transactions: dbTransactions() }, - db => db.transactions([], options), + db => db.transactions(TransactionGroups.confirmed, [], options), transactionsPage => { expect(transactionsPage.data[0].id).to.deep.equal(createObjectId(10)); expect(transactionsPage.data[1].id).to.deep.equal(createObjectId(20)); @@ -1557,7 +1601,7 @@ describe('catapult db', () => { // Act + Assert: return runDbTest( { transactions: dbTransactions() }, - db => db.transactions([], options), + db => db.transactions(TransactionGroups.confirmed, [], options), transactionsPage => { expect(transactionsPage.data[0].id).to.deep.equal(createObjectId(30)); expect(transactionsPage.data[1].id).to.deep.equal(createObjectId(20)); @@ -1578,7 +1622,7 @@ describe('catapult db', () => { // Act + Assert: return runDbTest( { transactions: dbTransactions() }, - db => db.transactions([], options), + db => db.transactions(TransactionGroups.confirmed, [], options), () => { expect(queryPagedDocumentsSpy.calledOnce).to.equal(true); expect(Object.keys(queryPagedDocumentsSpy.firstCall.args[2].$sort)[0]).to.equal('_id'); @@ -1714,7 +1758,7 @@ describe('catapult db', () => { return runDbTest( dbTransactions(), - db => db.transactions({ group }, paginationOptions), + db => db.transactions(group, {}, paginationOptions), transactionsPage => { const returnedIds = transactionsPage.data.map(t => t.id); expect(transactionsPage.data.length).to.equal(expectedObjectIds.length); @@ -1724,15 +1768,15 @@ describe('catapult db', () => { }); }; - runGroupTest('confirmed', [10]); - runGroupTest('partial', [20]); - runGroupTest('unconfirmed', [30]); + runGroupTest(TransactionGroups.confirmed, [10]); + runGroupTest(TransactionGroups.partial, [20]); + runGroupTest(TransactionGroups.unconfirmed, [30]); it('defaults to confirmed', () => // Act + Assert: runDbTest( dbTransactions(), - db => db.transactions({}, paginationOptions), + db => db.transactions(TransactionGroups.confirmed, {}, paginationOptions), transactionsPage => { expect(transactionsPage.data.length).to.equal(1); expect(transactionsPage.data[0].id).to.deep.equal(createObjectId(10)); diff --git a/rest/test/routes/allRoutes_spec.js b/rest/test/routes/allRoutes_spec.js index 7970677b0..a951064a2 100644 --- a/rest/test/routes/allRoutes_spec.js +++ b/rest/test/routes/allRoutes_spec.js @@ -62,9 +62,9 @@ describe('all routes', () => { '/node/storage', '/node/time', - '/transactions/:transactionId', - '/transactions/:hash/status', - '/transactions' + '/transactions/:group/:transactionId', + '/transactions/:group', + '/transactionStatus/:hash' ]); }); @@ -79,8 +79,8 @@ describe('all routes', () => { // Assert: test.assert.assertRoutes(routes, [ '/account', - '/transactions', - '/transactions/statuses' + '/transactions/:group', + '/transactionStatus' ]); }); diff --git a/rest/test/routes/transactionRoutes_spec.js b/rest/test/routes/transactionRoutes_spec.js index 5ba704219..b9e0904ec 100644 --- a/rest/test/routes/transactionRoutes_spec.js +++ b/rest/test/routes/transactionRoutes_spec.js @@ -29,288 +29,500 @@ const sinon = require('sinon'); const { address } = catapult.model; const { convert } = catapult.utils; +const TransactionGroups = { + confirmed: 'confirmed', + unconfirmed: 'unconfirmed', + partial: 'partial' +}; + describe('transaction routes', () => { - describe('transaction', () => { - describe('PUT transaction', () => { - test.route.packet.addPutPacketRouteTests(transactionRoutes.register, { - routeName: '/transactions', - packetType: '9', - inputs: { - valid: { - params: { payload: '123456' }, - parsed: Buffer.of( - 0x0B, 0x00, 0x00, 0x00, // size (header) - 0x09, 0x00, 0x00, 0x00, // type (header) - 0x12, 0x34, 0x56 // payload - ) - }, - invalid: { - params: { payload: '1234S6' }, - error: { key: 'payload' } - } - } - }); - }); + describe('transactions', () => { + const validObjectId = 'CCDDEEFF0011223344556677'; + const validHash = '112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00'; describe('get', () => { - const addGetPostTests = (dbApiName, key, ids, parsedIds) => { - const errorMessage = 'has an invalid format'; - test.route.document.addGetPostDocumentRouteTests(transactionRoutes.register, { - routes: { singular: '/transactions/:transactionId', plural: '/transactions' }, - inputs: { - valid: { object: { transactionId: ids[0] }, parsed: [parsedIds[0]], printable: ids[0] }, - validMultiple: { object: { transactionIds: ids }, parsed: parsedIds }, - invalid: { object: { transactionId: '12345' }, error: `transactionId ${errorMessage}` }, - invalidMultiple: { - object: { transactionIds: ['12345', ids[0], ids[1]] }, - error: `element in array transactionIds ${errorMessage}` - } - }, - dbApiName, - type: routeResultTypes.transaction + describe('by id', () => { + const fakeTransaction = { meta: { addresses: [] }, transaction: { type: 12345 } }; + + const dbTransactionsByIdsFake = sinon.fake.resolves(fakeTransaction); + const dbTransactionsByHashesFake = sinon.fake.resolves(fakeTransaction); + + const mockServer = new MockServer(); + const db = { + transactionsByIds: dbTransactionsByIdsFake, + transactionsByHashes: dbTransactionsByHashesFake + }; + transactionRoutes.register(mockServer.server, db, {}); + + const route = mockServer.getRoute('/transactions/:group/:transactionId').get(); + + beforeEach(() => { + mockServer.resetStats(); + dbTransactionsByIdsFake.resetHistory(); + dbTransactionsByHashesFake.resetHistory(); }); - }; - const addHomogeneousCheck = (validIds, invalidId) => { - it('does not support lookup of heterogenous ids', () => { - // Arrange: - const keyGroups = []; - const db = test.setup.createCapturingDb('transactionsByIds', keyGroups, [{ value: 'this is nonsense' }]); + const runParseArgumentParamTest = (params, parserName) => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed, transactionId: params } }; + const parseArgumentSpy = sinon.spy(routeUtils, 'parseArgument'); // Act: - const registerRoutes = transactionRoutes.register; - const ids = [validIds[0], validIds[1], invalidId, validIds[2]]; - const errorMessage = 'element in array transactionIds has an invalid format'; - return test.route.executeThrows( - registerRoutes, - '/transactions', - 'post', - { transactionIds: ids }, - db, - undefined, - errorMessage, - 409 - ); + return mockServer.callRoute(route, req).then(() => { + // Assert: + expect(parseArgumentSpy.calledOnceWith( + { group: TransactionGroups.confirmed, transactionId: params }, + 'transactionId', + parserName + )).to.equal(true); + parseArgumentSpy.restore(); + }); + }; + + it('throws if invalid length of transaction id', () => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed, transactionId: '12345' } }; + + // Act + Assert: + expect(() => mockServer.callRoute(route, req)).to.throw('invalid length of transaction id \'12345\''); }); - }; - const Valid_Object_Ids = ['00112233445566778899AABB', 'CCDDEEFF0011223344556677', '8899AABBCCDDEEFF00112233']; - const Valid_Transaction_Hashes = [ - '00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF', - '112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00', - '2233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF0011' - ]; + it('calls parseArgument with correct parser for id', () => runParseArgumentParamTest(validObjectId, 'objectId')); + it('calls parseArgument with correct parser for hash', () => runParseArgumentParamTest(validHash, 'hash256')); - describe('objectId', () => { - addGetPostTests('transactionsByIds', 'transactionId', Valid_Object_Ids, Valid_Object_Ids); - addHomogeneousCheck(Valid_Object_Ids, Valid_Transaction_Hashes[0]); - }); + describe('checks correct group is provided', () => { + const runValidGroupTest = group => { + it(`${group} - by id`, () => { + // Arrange + const req = { params: { group, transactionId: validObjectId } }; - describe('transactionHash', () => { - addGetPostTests( - 'transactionsByHashes', - 'transactionId', - Valid_Transaction_Hashes, - Valid_Transaction_Hashes.map(convert.hexToUint8) - ); - addHomogeneousCheck(Valid_Transaction_Hashes, Valid_Object_Ids[0]); - }); - }); - }); + // Act: + mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionsByIdsFake.calledOnce).to.equal(true); + expect(dbTransactionsByIdsFake.firstCall.args[0]).to.equal(group); + }); + }); - describe('transactions', () => { - describe('get', () => { - const testAddressString = 'SBZ22LWA7GDZLPLQF7PXTMNLWSEZ7ZRVGRMWLXQ'; - const testAddress = address.stringToAddress(testAddressString); - - const testPublickeyString = '7DE16AEDF57EB9561D3E6EFA4AE66F27ABDA8AEC8BC020B6277360E31619DCE7'; - const testPublickey = convert.hexToUint8(testPublickeyString); - - const fakeTransaction = { meta: { addresses: [] }, transaction: { type: 12345 } }; - const fakePaginatedTransaction = { - data: [fakeTransaction], - pagination: { - pageNumber: 1, - pageSize: 10, - totalEntries: 1, - totalPages: 1 - } - }; - const dbTransactionsFake = sinon.fake.resolves(fakePaginatedTransaction); + it(`${group} - by hash`, () => { + // Arrange + const req = { params: { group, transactionId: validHash } }; - const mockServer = new MockServer(); - const db = { transactions: dbTransactionsFake }; - const services = { - config: { - pageSize: { - min: 10, - max: 100, - default: 20 - } - } - }; - transactionRoutes.register(mockServer.server, db, services); + // Act: + mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionsByHashesFake.calledOnce).to.equal(true); + expect(dbTransactionsByHashesFake.firstCall.args[0]).to.equal(group); + }); + }); + }; - const route = mockServer.getRoute('/transactions').get(); + Object.keys(TransactionGroups).forEach(group => runValidGroupTest(group)); - beforeEach(() => { - mockServer.resetStats(); - dbTransactionsFake.resetHistory(); - }); + it('not found if group does not exists', () => { + // Arrange + const req = { params: { group: 'nonExistingGroup', transactionId: validObjectId } }; - describe('returns transactions', () => { - it('returns correct structure with transactions', () => { - const req = { - params: {} - }; + // Act: + mockServer.callRoute(route, req); - // Act: - return mockServer.callRoute(route, req).then(() => { // Assert: - expect(mockServer.send.firstCall.args[0]).to.deep.equal({ - payload: fakePaginatedTransaction, - type: routeResultTypes.transaction, - structure: 'page' - }); expect(mockServer.next.calledOnce).to.equal(true); + expect(mockServer.next.firstCall.args[0].statusCode).to.equal(404); }); }); - }); - describe('parses filters', () => { - const runParseFilterTest = (filter, param, value) => { - it(filter, () => { - const req = { params: { [filter]: param } }; - - const expectedResult = { - address: undefined, - height: undefined, - recipientAddress: undefined, - signerPublicKey: undefined, - group: undefined, - embedded: undefined, - transactionTypes: undefined - }; + describe('calls correct transaction retriever with correct params', () => { + it('id param', () => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed, transactionId: validObjectId } }; - expectedResult[filter] = value; + // Act: + return mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionsByIdsFake.calledOnce).to.equal(true); + expect(dbTransactionsByIdsFake.firstCall.args[1]).to.deep.equal([validObjectId]); - // Act + Assert + expect(mockServer.send.firstCall.args[0]).to.deep.equal({ + payload: fakeTransaction, + type: routeResultTypes.transaction + }); + expect(mockServer.next.calledOnce).to.equal(true); + }); + }); + + it('hash param', () => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed, transactionId: validHash } }; + + // Act: return mockServer.callRoute(route, req).then(() => { - expect(dbTransactionsFake.firstCall.args[0]).to.deep.equal(expectedResult); + // Assert: + expect(dbTransactionsByHashesFake.calledOnce).to.equal(true); + expect(dbTransactionsByHashesFake.firstCall.args[1]).to.deep.equal([convert.hexToUint8(validHash)]); + + expect(mockServer.send.firstCall.args[0]).to.deep.equal({ + payload: fakeTransaction, + type: routeResultTypes.transaction + }); + expect(mockServer.next.calledOnce).to.equal(true); }); }); + }); + }); + + describe('paginated', () => { + const testAddressString = 'SBZ22LWA7GDZLPLQF7PXTMNLWSEZ7ZRVGRMWLXQ'; + const testAddress = address.stringToAddress(testAddressString); + + const testPublickeyString = '7DE16AEDF57EB9561D3E6EFA4AE66F27ABDA8AEC8BC020B6277360E31619DCE7'; + const testPublickey = convert.hexToUint8(testPublickeyString); + + const fakeTransaction = { meta: { addresses: [] }, transaction: { type: 12345 } }; + const fakePaginatedTransaction = { + data: [fakeTransaction], + pagination: { + pageNumber: 1, + pageSize: 10, + totalEntries: 1, + totalPages: 1 + } + }; + const dbTransactionsFake = sinon.fake.resolves(fakePaginatedTransaction); + + const mockServer = new MockServer(); + const db = { transactions: dbTransactionsFake }; + const services = { + config: { + pageSize: { + min: 10, + max: 100, + default: 20 + } + } }; + transactionRoutes.register(mockServer.server, db, services); + + const route = mockServer.getRoute('/transactions/:group').get(); - const testCases = [ - { filter: 'height', param: '15', value: 15 }, - { filter: 'address', param: testAddressString, value: testAddress }, - { filter: 'signerPublicKey', param: testPublickeyString, value: testPublickey }, - { filter: 'recipientAddress', param: testAddressString, value: testAddress }, - { filter: 'embedded', param: 'true', value: true }, - { filter: 'group', param: 'confirmed', value: 'confirmed' } - ]; - - testCases.forEach(testCase => { - runParseFilterTest(testCase.filter, testCase.param, testCase.value); + beforeEach(() => { + mockServer.resetStats(); + dbTransactionsFake.resetHistory(); }); - it('transactionTypes', () => { - const req = { params: { type: ['1', '5', '25'] } }; + describe('returns transactions', () => { + it('returns correct structure with transactions', () => { + const req = { + params: { group: TransactionGroups.confirmed } + }; - // Act + Assert - return mockServer.callRoute(route, req).then(() => { - expect(dbTransactionsFake.firstCall.args[0]).to.deep.equal({ - address: undefined, - height: undefined, - recipientAddress: undefined, - signerPublicKey: undefined, - group: undefined, - embedded: undefined, - transactionTypes: [1, 5, 25] + // Act: + return mockServer.callRoute(route, req).then(() => { + // Assert: + expect(mockServer.send.firstCall.args[0]).to.deep.equal({ + payload: fakePaginatedTransaction, + type: routeResultTypes.transaction, + structure: 'page' + }); + expect(mockServer.next.calledOnce).to.equal(true); }); }); }); - }); - describe('parses options', () => { - it('parses and forwards paging options', () => { - // Arrange: - const pagingBag = 'fakePagingBagObject'; - const paginationParser = sinon.stub(routeUtils, 'parsePaginationArguments').returns(pagingBag); + describe('parses filters', () => { + const runParseFilterTest = (filter, param, value) => { + it(filter, () => { + const req = { params: { group: TransactionGroups.confirmed, [filter]: param } }; - // Act: - return mockServer.callRoute(route, { params: {} }).then(() => { - // Assert: - expect(dbTransactionsFake.calledOnce).to.equal(true); - expect(dbTransactionsFake.firstCall.args[1]).to.deep.equal(pagingBag); - paginationParser.restore(); + const expectedResult = { + address: undefined, + height: undefined, + recipientAddress: undefined, + signerPublicKey: undefined, + embedded: undefined, + transactionTypes: undefined + }; + + expectedResult[filter] = value; + + // Act + Assert + return mockServer.callRoute(route, req).then(() => { + expect(dbTransactionsFake.firstCall.args[1]).to.deep.equal(expectedResult); + }); + }); + }; + + const testCases = [ + { filter: 'height', param: '15', value: 15 }, + { filter: 'address', param: testAddressString, value: testAddress }, + { filter: 'signerPublicKey', param: testPublickeyString, value: testPublickey }, + { filter: 'recipientAddress', param: testAddressString, value: testAddress }, + { filter: 'embedded', param: 'true', value: true } + ]; + + testCases.forEach(testCase => { + runParseFilterTest(testCase.filter, testCase.param, testCase.value); + }); + + it('transactionTypes', () => { + const req = { params: { group: TransactionGroups.confirmed, type: ['1', '5', '25'] } }; + + // Act + Assert + return mockServer.callRoute(route, req).then(() => { + expect(dbTransactionsFake.firstCall.args[1]).to.deep.equal({ + address: undefined, + height: undefined, + recipientAddress: undefined, + signerPublicKey: undefined, + embedded: undefined, + transactionTypes: [1, 5, 25] + }); + }); }); }); - it('allowed sort fields are taken into account', () => { - // Arrange: - const paginationParserSpy = sinon.spy(routeUtils, 'parsePaginationArguments'); - const expectedAllowedSortFields = { id: 'objectId' }; + describe('parses options', () => { + it('parses and forwards paging options', () => { + // Arrange: + const pagingBag = 'fakePagingBagObject'; + const paginationParser = sinon.stub(routeUtils, 'parsePaginationArguments').returns(pagingBag); + + // Act: + return mockServer.callRoute(route, { params: { group: TransactionGroups.confirmed } }).then(() => { + // Assert: + expect(dbTransactionsFake.calledOnce).to.equal(true); + expect(dbTransactionsFake.firstCall.args[2]).to.deep.equal(pagingBag); + paginationParser.restore(); + }); + }); - // Act: - return mockServer.callRoute(route, { params: {} }).then(() => { - // Assert: - expect(paginationParserSpy.calledOnce).to.equal(true); - expect(paginationParserSpy.firstCall.args[2]).to.deep.equal(expectedAllowedSortFields); - paginationParserSpy.restore(); + it('allowed sort fields are taken into account', () => { + // Arrange: + const paginationParserSpy = sinon.spy(routeUtils, 'parsePaginationArguments'); + const expectedAllowedSortFields = { id: 'objectId' }; + + // Act: + return mockServer.callRoute(route, { params: { group: TransactionGroups.confirmed } }).then(() => { + // Assert: + expect(paginationParserSpy.calledOnce).to.equal(true); + expect(paginationParserSpy.firstCall.args[2]).to.deep.equal(expectedAllowedSortFields); + paginationParserSpy.restore(); + }); }); }); - }); - describe('does not allow filtering by address if signerPublicKey or recipientAddress are provided', () => { - const errorMessage = 'can\'t filter by address if signerPublicKey or recipientAddress are already provided'; + describe('does not allow filtering by address if signerPublicKey or recipientAddress are provided', () => { + const errorMessage = 'can\'t filter by address if signerPublicKey or recipientAddress are already provided'; - it('address and signer public key', () => { - const req = { - params: { address: testAddressString, signerPublicKey: testPublickeyString } - }; + it('address and signer public key', () => { + const req = { + params: { group: TransactionGroups.confirmed, address: testAddressString, signerPublicKey: testPublickeyString } + }; - // Act + Assert - expect(() => mockServer.callRoute(route, req)).to.throw(errorMessage); + // Act + Assert + expect(() => mockServer.callRoute(route, req)).to.throw(errorMessage); + }); + + it('address and recipient address', () => { + const req = { + params: { group: TransactionGroups.confirmed, address: testAddressString, recipientAddress: testAddressString } + }; + + // Act + Assert + expect(() => mockServer.callRoute(route, req)).to.throw(errorMessage); + }); }); - it('address and recipient address', () => { - const req = { - params: { address: testAddressString, recipientAddress: testAddressString } + describe('checks correct group is provided', () => { + const runValidGroupTest = group => { + it(group, () => + // Act + Assert + mockServer.callRoute(route, { params: { group } }).then(() => { + expect(dbTransactionsFake.firstCall.args[1]).to.deep.equal({ + address: undefined, + height: undefined, + recipientAddress: undefined, + signerPublicKey: undefined, + embedded: undefined, + transactionTypes: undefined + }); + })); }; - // Act + Assert - expect(() => mockServer.callRoute(route, req)).to.throw(errorMessage); + Object.keys(TransactionGroups).forEach(group => runValidGroupTest(group)); + + it('not found if group does not exists', () => { + // Arrange: + const req = { params: { group: 'nonExistingGroup' } }; + + // Act: + mockServer.callRoute(route, req); + + // Assert: + expect(mockServer.next.calledOnce).to.equal(true); + expect(mockServer.next.firstCall.args[0].statusCode).to.equal(404); + }); }); }); + }); + + describe('post', () => { + const fakeTransactions = [{ meta: { addresses: [] }, transaction: { type: 12345 } }]; + + const dbTransactionsByIdsFake = sinon.fake.resolves(fakeTransactions); + const dbTransactionsByHashesFake = sinon.fake.resolves(fakeTransactions); + + const mockServer = new MockServer(); + const db = { + transactionsByIds: dbTransactionsByIdsFake, + transactionsByHashes: dbTransactionsByHashesFake + }; + transactionRoutes.register(mockServer.server, db, {}); + + const route = mockServer.getRoute('/transactions/:group').post(); + + beforeEach(() => { + mockServer.resetStats(); + dbTransactionsByIdsFake.resetHistory(); + dbTransactionsByHashesFake.resetHistory(); + }); + + it('throws if ids or hashes are not provided', () => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed } }; + + // Act + Assert: + expect(() => mockServer.callRoute(route, req)).to.throw('either ids or hashes must be provided'); + }); + + it('throws if both ids and hashes are provided', () => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed, transactionIds: [], hashes: [] } }; + + // Act + Assert: + expect(() => mockServer.callRoute(route, req)).to.throw('either ids or hashes must be provided'); + }); describe('checks correct group is provided', () => { const runValidGroupTest = group => { - it(group, () => - // Act + Assert - mockServer.callRoute(route, { params: { group } }).then(() => { - expect(dbTransactionsFake.firstCall.args[0]).to.deep.equal({ - address: undefined, - height: undefined, - recipientAddress: undefined, - signerPublicKey: undefined, - group, - embedded: undefined, - transactionTypes: undefined - }); - })); + it(`${group} - by ids`, () => { + // Arrange + const req = { params: { group, transactionIds: [validObjectId] } }; + + // Act: + mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionsByIdsFake.calledOnce).to.equal(true); + expect(dbTransactionsByIdsFake.firstCall.args[0]).to.equal(group); + }); + }); + it(`${group} - by hashes`, () => { + // Arrange + const req = { params: { group, hashes: [validHash] } }; + + // Act: + mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionsByHashesFake.calledOnce).to.equal(true); + expect(dbTransactionsByHashesFake.firstCall.args[0]).to.equal(group); + }); + }); }; - ['confirmed', 'unconfirmed', 'partial'].forEach(group => runValidGroupTest(group)); + Object.keys(TransactionGroups).forEach(group => runValidGroupTest(group)); - it('invalid', () => { - const req = { - params: { group: 'nonsenseGroup' } - }; + it('not found if group does not exists', () => { + // Arrange + const req = { params: { group: 'nonExistingGroup', transactionIds: [validObjectId] } }; + + // Act: + mockServer.callRoute(route, req); + + // Assert: + expect(mockServer.next.calledOnce).to.equal(true); + expect(mockServer.next.firstCall.args[0].statusCode).to.equal(404); + }); + }); - // Act + Assert - expect(() => mockServer.callRoute(route, req)).to.throw('invalid transaction group provided'); + const runParseArgumentAsArrayParamTest = (paramValues, paramName, parserName) => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed, [paramName]: paramValues } }; + const parseArgumentsAsArraySpy = sinon.spy(routeUtils, 'parseArgumentAsArray'); + + // Act: + return mockServer.callRoute(route, req).then(() => { + // Assert: + expect(parseArgumentsAsArraySpy.calledOnceWith( + { group: TransactionGroups.confirmed, [paramName]: paramValues }, + paramName, + parserName + )).to.equal(true); + parseArgumentsAsArraySpy.restore(); }); + }; + + it('calls parseArgumentAsArray with correct parser for ids', () => + runParseArgumentAsArrayParamTest([validObjectId, validObjectId], 'transactionIds', 'objectId')); + it('calls parseArgumentAsArray with correct parser for hashes', () => + runParseArgumentAsArrayParamTest([validHash, validHash], 'hashes', 'hash256')); + + describe('calls correct transaction retriever with correct params', () => { + it('id params', () => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed, transactionIds: [validObjectId] } }; + + // Act: + return mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionsByIdsFake.calledOnce).to.equal(true); + expect(dbTransactionsByIdsFake.firstCall.args[1]).to.deep.equal([validObjectId]); + + expect(mockServer.send.firstCall.args[0]).to.deep.equal({ + payload: fakeTransactions, + type: routeResultTypes.transaction + }); + expect(mockServer.next.calledOnce).to.equal(true); + }); + }); + + it('hash params', () => { + // Arrange + const req = { params: { group: TransactionGroups.confirmed, hashes: [validHash] } }; + + // Act: + return mockServer.callRoute(route, req).then(() => { + // Assert: + expect(dbTransactionsByHashesFake.calledOnce).to.equal(true); + expect(dbTransactionsByHashesFake.firstCall.args[1]).to.deep.equal([convert.hexToUint8(validHash)]); + + expect(mockServer.send.firstCall.args[0]).to.deep.equal({ + payload: fakeTransactions, + type: routeResultTypes.transaction + }); + expect(mockServer.next.calledOnce).to.equal(true); + }); + }); + }); + }); + + describe('put', () => { + test.route.packet.addPutPacketRouteTests(transactionRoutes.register, { + routeName: '/transactions', + packetType: '9', + inputs: { + valid: { + params: { payload: '123456' }, + parsed: Buffer.of( + 0x0B, 0x00, 0x00, 0x00, // size (header) + 0x09, 0x00, 0x00, 0x00, // type (header) + 0x12, 0x34, 0x56 // payload + ) + }, + invalid: { + params: { payload: '1234S6' }, + error: { key: 'payload' } + } + } }); }); }); diff --git a/rest/test/routes/transactionStatusRoutes_spec.js b/rest/test/routes/transactionStatusRoutes_spec.js index 7b5c9a5bb..5479741ca 100644 --- a/rest/test/routes/transactionStatusRoutes_spec.js +++ b/rest/test/routes/transactionStatusRoutes_spec.js @@ -41,13 +41,9 @@ describe('transaction status routes', () => { const services = { config: { transactionStates: [] } }; const routeInfo = { - base: '/transactions', + base: '/transactionStatus', singular: 'hash', - plural: 'hashes', - postfixes: { - singular: 'status', - plural: 'statuses' - } + plural: 'hashes' }; let addGetPostDocumentRoutesSpy = null;