diff --git a/test/app.e2e-context.ts b/test/app.e2e-context.ts new file mode 100644 index 0000000..3d371ad --- /dev/null +++ b/test/app.e2e-context.ts @@ -0,0 +1,91 @@ +import { Server } from 'http'; +import { Observer } from 'rxjs'; +import { AxiosResponse } from 'axios'; +import { HttpService } from '@nestjs/axios'; +import request from 'superwstest'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { WsAdapter } from '@nestjs/platform-ws'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AppModule } from '../src/app.module'; +import { EventStreamReply , Event } from '../src/event-stream/event-stream.interfaces'; +import { EventStreamService } from '../src/event-stream/event-stream.service'; +import { EventStreamProxyGateway } from '../src/eventstream-proxy/eventstream-proxy.gateway'; +import { TokensService } from '../src/tokens/tokens.service'; + +export const BASE_URL = 'http://eth'; +export const INSTANCE_PATH = '/tokens'; +export const PREFIX = 'fly'; +export const TOPIC = 'tokentest'; + +export class TestContext { + app: INestApplication; + server: ReturnType; + http: { + get: ReturnType; + post: ReturnType; + }; + eventHandler: (events: Event[]) => void; + receiptHandler: (receipt: EventStreamReply) => void; + + eventstream = { + connect: ( + url: string, + topic: string, + handleEvents: (events: Event[]) => void, + handleReceipt: (receipt: EventStreamReply) => void, + ) => { + this.eventHandler = handleEvents; + this.receiptHandler = handleReceipt; + }, + + getSubscription: jest.fn(), + }; + + async begin() { + this.http = { + get: jest.fn(), + post: jest.fn(), + }; + this.eventstream.getSubscription.mockReset(); + + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }) + .overrideProvider(HttpService) + .useValue(this.http) + .overrideProvider(EventStreamService) + .useValue(this.eventstream) + .compile(); + + this.app = moduleFixture.createNestApplication(); + this.app.useWebSocketAdapter(new WsAdapter(this.app)); + this.app.useGlobalPipes(new ValidationPipe({ whitelist: true })); + await this.app.init(); + + this.app.get(EventStreamProxyGateway).configure('url', TOPIC); + this.app.get(TokensService).configure(BASE_URL, INSTANCE_PATH, TOPIC, PREFIX, '', ''); + + (this.app.getHttpServer() as Server).listen(); + this.server = request(this.app.getHttpServer()); + } + + async end() { + await this.app.close(); + } +} + +export class FakeObservable { + constructor(public data: T) {} + + subscribe(observer?: Partial>>) { + observer?.next && + observer?.next({ + status: 200, + statusText: 'OK', + headers: {}, + config: {}, + data: this.data, + }); + observer?.complete && observer?.complete(); + } +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index c5d75d6..5d6f0d6 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -14,1341 +14,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Server } from 'http'; -import { Observer } from 'rxjs'; -import { AxiosResponse } from 'axios'; -import { HttpService } from '@nestjs/axios'; -import { INestApplication, ValidationPipe } from '@nestjs/common'; -import { WsAdapter } from '@nestjs/platform-ws'; -import { Test, TestingModule } from '@nestjs/testing'; -import request from 'superwstest'; -import { - Event, - EventStreamReply, - EventStreamSubscription, -} from '../src/event-stream/event-stream.interfaces'; -import { EventStreamService } from '../src/event-stream/event-stream.service'; -import { EventStreamProxyGateway } from '../src/eventstream-proxy/eventstream-proxy.gateway'; -import { ReceiptEvent } from '../src/eventstream-proxy/eventstream-proxy.interfaces'; -import { - ApprovalForAllEvent, - EthConnectAsyncResponse, - EthConnectReturn, - TokenApproval, - TokenApprovalEvent, - TokenBalance, - TokenBalanceQuery, - TokenBurn, - TokenBurnEvent, - TokenCreateEvent, - TokenMint, - TokenMintEvent, - TokenPool, - TokenPoolEvent, - TokenTransfer, - TokenTransferEvent, - TokenType, - TransferBatchEvent, - TransferSingleEvent, -} from '../src/tokens/tokens.interfaces'; -import { TokensService } from '../src/tokens/tokens.service'; -import { WebSocketMessage } from '../src/websocket-events/websocket-events.base'; -import { packSubscriptionName } from '../src/tokens/tokens.util'; -import { AppModule } from './../src/app.module'; - -const BASE_URL = 'http://eth'; -const INSTANCE_PATH = '/tokens'; -const IDENTITY = '0x1'; -const TOPIC = 'tokentest'; -const PREFIX = 'fly'; -const OPTIONS = { - params: { - 'fly-from': IDENTITY, - 'fly-sync': 'false', - }, -}; -const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; - -const tokenCreateEventSignature = 'TokenCreate(address,uint256,bytes)'; -const transferSingleEventSignature = 'TransferSingle(address,address,address,uint256,uint256)'; -const approvalForAllEventSignature = 'ApprovalForAll(address,address,bool)'; -const transferBatchEventSignature = 'TransferBatch(address,address,address,uint256[],uint256[])'; - -class FakeObservable { - constructor(public data: T) {} - - subscribe(observer?: Partial>>) { - observer?.next && - observer?.next({ - status: 200, - statusText: 'OK', - headers: {}, - config: {}, - data: this.data, - }); - observer?.complete && observer?.complete(); - } -} +import { TestContext } from './app.e2e-context'; +import SuiteApi from './suites/api'; +import SuiteWebsocket from './suites/websocket'; describe('AppController (e2e)', () => { - let app: INestApplication; - let server: ReturnType; - let http: { - get: ReturnType; - post: ReturnType; - }; - let eventHandler: (events: Event[]) => void; - let receiptHandler: (receipt: EventStreamReply) => void; - - const eventstream = { - connect: ( - url: string, - topic: string, - handleEvents: (events: Event[]) => void, - handleReceipt: (receipt: EventStreamReply) => void, - ) => { - eventHandler = handleEvents; - receiptHandler = handleReceipt; - }, - - getSubscription: jest.fn(), - }; - - beforeEach(async () => { - http = { - get: jest.fn(), - post: jest.fn(), - }; - eventstream.getSubscription.mockReset(); - - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule], - }) - .overrideProvider(HttpService) - .useValue(http) - .overrideProvider(EventStreamService) - .useValue(eventstream) - .compile(); - - app = moduleFixture.createNestApplication(); - app.useWebSocketAdapter(new WsAdapter(app)); - app.useGlobalPipes(new ValidationPipe({ whitelist: true })); - await app.init(); - - app.get(EventStreamProxyGateway).configure('url', TOPIC); - app.get(TokensService).configure(BASE_URL, INSTANCE_PATH, TOPIC, PREFIX, '', ''); - - (app.getHttpServer() as Server).listen(); - server = request(app.getHttpServer()); - }); - - afterEach(async () => { - await app.close(); - }); - - it('Create fungible pool', async () => { - const request: TokenPool = { - type: TokenType.FUNGIBLE, - requestId: 'op1', - data: 'tx1', - signer: IDENTITY, - }; - const response: EthConnectAsyncResponse = { - id: 'op1', - sent: true, - }; - - http.post = jest.fn(() => new FakeObservable(response)); - - await server.post('/createpool').send(request).expect(202).expect({ id: 'op1' }); - - expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith( - `${BASE_URL}${INSTANCE_PATH}/create`, - { - data: '0x747831', - is_fungible: true, - }, - { - ...OPTIONS, - params: { - ...OPTIONS.params, - 'fly-id': 'op1', - }, - }, - ); - }); - - it('Create non-fungible pool', async () => { - const request: TokenPool = { - type: TokenType.NONFUNGIBLE, - signer: '0xabc', - }; - const response: EthConnectAsyncResponse = { - id: '1', - sent: true, - }; - - http.post = jest.fn(() => new FakeObservable(response)); - - await server.post('/createpool').send(request).expect(202).expect({ id: '1' }); - - expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith( - `${BASE_URL}${INSTANCE_PATH}/create`, - { - data: '0x00', - is_fungible: false, - }, - { - ...OPTIONS, - params: { - ...OPTIONS.params, - 'fly-from': '0xabc', - }, - }, - ); - }); - - it('Create pool - unrecognized fields', async () => { - const request = { - type: TokenType.FUNGIBLE, - signer: IDENTITY, - isBestPool: true, // will be stripped but will not cause an error - }; - const response: EthConnectAsyncResponse = { - id: 'op1', - sent: true, - }; - - http.post = jest.fn(() => new FakeObservable(response)); - - await server.post('/createpool').send(request).expect(202).expect({ id: 'op1' }); - }); - - it('Mint fungible token', async () => { - const request: TokenMint = { - poolLocator: 'F1', - to: '1', - amount: '2', - data: 'test', - signer: IDENTITY, - }; - const response: EthConnectAsyncResponse = { - id: '1', - sent: true, - }; - - http.post = jest.fn(() => new FakeObservable(response)); - - await server.post('/mint').send(request).expect(202).expect({ id: '1' }); - - expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith( - `${BASE_URL}${INSTANCE_PATH}/mintFungible`, - { - type_id: '340282366920938463463374607431768211456', - to: ['1'], - amounts: ['2'], - data: '0x74657374', - }, - OPTIONS, - ); - }); - - it('Mint non-fungible token', async () => { - const request: TokenMint = { - poolLocator: 'N1', - to: '1', - amount: '2', - signer: IDENTITY, - }; - const response: EthConnectAsyncResponse = { - id: '1', - sent: true, - }; - - http.post = jest.fn(() => new FakeObservable(response)); - - await server.post('/mint').send(request).expect(202).expect({ id: '1' }); - - expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith( - `${BASE_URL}${INSTANCE_PATH}/mintNonFungible`, - { - type_id: '57896044618658097711785492504343953926975274699741220483192166611388333031424', - to: ['1', '1'], - data: '0x00', - }, - OPTIONS, - ); - }); - - it('Burn token', async () => { - const request: TokenBurn = { - poolLocator: 'N1', - tokenIndex: '1', - from: 'A', - amount: '1', - data: 'tx1', - signer: IDENTITY, - }; - const response: EthConnectAsyncResponse = { - id: '1', - sent: true, - }; - - http.post = jest.fn(() => new FakeObservable(response)); - - await server.post('/burn').send(request).expect(202).expect({ id: '1' }); - - expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith( - `${BASE_URL}${INSTANCE_PATH}/burn`, - { - id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', - from: 'A', - amount: '1', - data: '0x747831', - }, - OPTIONS, - ); - }); - - it('Transfer token', async () => { - const request: TokenTransfer = { - poolLocator: 'F1', - from: '1', - to: '2', - amount: '2', - signer: IDENTITY, - }; - const response: EthConnectAsyncResponse = { - id: '1', - sent: true, - }; - - http.post = jest.fn(() => new FakeObservable(response)); - - await server.post('/transfer').send(request).expect(202).expect({ id: '1' }); - - expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith( - `${BASE_URL}${INSTANCE_PATH}/safeTransferFrom`, - { - id: '340282366920938463463374607431768211456', - from: '1', - to: '2', - amount: '2', - data: '0x00', - }, - OPTIONS, - ); - }); - - it('Token approval', async () => { - const request: TokenApproval = { - poolLocator: 'F1', - signer: IDENTITY, - operator: '2', - approved: true, - }; - const response: EthConnectAsyncResponse = { - id: '1', - sent: true, - }; - - http.post = jest.fn(() => new FakeObservable(response)); - - await server.post('/approval').send(request).expect(202).expect({ id: '1' }); - - expect(http.post).toHaveBeenCalledTimes(1); - expect(http.post).toHaveBeenCalledWith( - `${BASE_URL}${INSTANCE_PATH}/setApprovalForAllWithData`, - { - operator: '2', - approved: true, - data: '0x00', - }, - OPTIONS, - ); - }); - - it('Query balance', async () => { - const request: TokenBalanceQuery = { - account: '1', - poolLocator: 'F1', - tokenIndex: '0', - }; - const response: EthConnectReturn = { - output: '1', - }; - - http.get = jest.fn(() => new FakeObservable(response)); - - await server - .get('/balance') - .query(request) - .expect(200) - .expect({ - balance: '1', - }); - - expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/balanceOf`, { - params: { - account: '1', - id: '340282366920938463463374607431768211456', - }, - }); - }); - - it('Websocket: token pool event', () => { - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'F1', ''), - }); - - return server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - subId: 'sb123', - signature: tokenCreateEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - timestamp: '2020-01-01 00:00:00Z', - data: { - operator: 'bob', - type_id: '340282366920938463463374607431768211456', - data: '0x00', - }, - }, - ]); - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-pool', - data: { - standard: 'ERC1155', - poolLocator: 'id=F1&block=1', - type: 'fungible', - signer: 'bob', - data: '', - info: { - address: '0x00001', - typeId: '0x0000000000000000000000000000000100000000000000000000000000000000', - }, - blockchain: { - id: '000000000001/000000/000000', - name: 'TokenCreate', - location: 'address=0x00001', - signature: tokenCreateEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - operator: 'bob', - type_id: '340282366920938463463374607431768211456', - data: '0x00', - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - signature: tokenCreateEventSignature, - }, - }, - }, - }); - return true; - }); - }); + const context = new TestContext(); - it('Websocket: token pool event from base subscription', () => { - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'base', ''), + function addSuite(name: string, suite: (context: TestContext) => void) { + describe(name, () => { + beforeEach(() => context.begin()); + afterEach(() => context.end()); + suite(context); }); + } - return server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - subId: 'sb123', - signature: tokenCreateEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - timestamp: '2020-01-01 00:00:00Z', - data: { - operator: 'bob', - type_id: '340282366920938463463374607431768211456', - data: '0x00', - }, - }, - ]); - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-pool', - data: { - standard: 'ERC1155', - poolLocator: 'id=F1&block=1', - type: 'fungible', - signer: 'bob', - data: '', - info: { - address: '0x00001', - typeId: '0x0000000000000000000000000000000100000000000000000000000000000000', - }, - blockchain: { - id: '000000000001/000000/000000', - name: 'TokenCreate', - location: 'address=0x00001', - signature: tokenCreateEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - operator: 'bob', - type_id: '340282366920938463463374607431768211456', - data: '0x00', - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - signature: tokenCreateEventSignature, - }, - }, - }, - }); - return true; - }); - }); - - it('Websocket: token mint event', async () => { - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''), - }); - - http.get = jest.fn( - () => - new FakeObservable({ - output: 'firefly://token/{id}', - }), - ); - - await server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - subId: 'sb-123', - signature: transferSingleEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - id: '340282366920938463463374607431768211456', - from: ZERO_ADDRESS, - to: 'A', - operator: 'A', - value: '5', - transaction: { - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - }, - }, - inputMethod: 'mintFungible', - inputArgs: { - data: '0x74657374', - }, - }, - ]); - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-mint', - data: { - id: '000000000001/000000/000001', - poolLocator: 'id=F1&block=1', - to: 'A', - amount: '5', - signer: 'A', - uri: 'firefly://token/0000000000000000000000000000000100000000000000000000000000000000', - data: 'test', - blockchain: { - id: '000000000001/000000/000001', - name: 'TransferSingle', - location: 'address=0x00001', - signature: transferSingleEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - id: '340282366920938463463374607431768211456', - from: ZERO_ADDRESS, - to: 'A', - operator: 'A', - value: '5', - transaction: { - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - }, - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - signature: transferSingleEventSignature, - }, - }, - }, - }); - return true; - }); - - expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); - }); - - it('Websocket: token mint event with old pool ID', async () => { - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'F1', ''), - }); - - http.get = jest.fn( - () => - new FakeObservable({ - output: 'firefly://token/{id}', - }), - ); - - await server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - subId: 'sb-123', - signature: transferSingleEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - id: '340282366920938463463374607431768211456', - from: ZERO_ADDRESS, - to: 'A', - operator: 'A', - value: '5', - transaction: { - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - }, - }, - inputMethod: 'mintFungible', - inputArgs: { - data: '0x74657374', - }, - }, - ]); - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-mint', - data: { - id: '000000000001/000000/000001', - poolLocator: 'F1', - to: 'A', - amount: '5', - signer: 'A', - uri: 'firefly://token/0000000000000000000000000000000100000000000000000000000000000000', - data: 'test', - blockchain: { - id: '000000000001/000000/000001', - name: 'TransferSingle', - location: 'address=0x00001', - signature: transferSingleEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - id: '340282366920938463463374607431768211456', - from: ZERO_ADDRESS, - to: 'A', - operator: 'A', - value: '5', - transaction: { - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - }, - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - signature: transferSingleEventSignature, - }, - }, - }, - }); - return true; - }); - - expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); - }); - - it('Websocket: token burn event', async () => { - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'id=N1&block=1', ''), - }); - - http.get = jest.fn( - () => - new FakeObservable({ - output: 'firefly://token/{id}', - }), - ); - - await server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - subId: 'sb-123', - signature: transferSingleEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', - from: 'A', - to: ZERO_ADDRESS, - operator: 'A', - value: '1', - transaction: { - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - }, - }, - inputMethod: 'burn', - inputArgs: { - data: '0x74657374', - }, - }, - ]); - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-burn', - data: { - id: '000000000001/000000/000001', - poolLocator: 'id=N1&block=1', - tokenIndex: '1', - from: 'A', - amount: '1', - signer: 'A', - uri: 'firefly://token/8000000000000000000000000000000100000000000000000000000000000001', - data: 'test', - blockchain: { - id: '000000000001/000000/000001', - name: 'TransferSingle', - location: 'address=0x00001', - signature: transferSingleEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', - from: 'A', - to: ZERO_ADDRESS, - operator: 'A', - value: '1', - transaction: { - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - }, - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - signature: transferSingleEventSignature, - }, - }, - }, - }); - return true; - }); - - expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); - }); - - it('Websocket: token transfer event', async () => { - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'id=N1&block=1', ''), - }); - - http.get = jest.fn( - () => - new FakeObservable({ - output: 'firefly://token/{id}', - }), - ); - - await server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - subId: 'sb123', - signature: transferSingleEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', - from: 'A', - to: 'B', - operator: 'A', - value: '1', - }, - }, - ]); - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-transfer', - data: { - id: '000000000001/000000/000001', - poolLocator: 'id=N1&block=1', - tokenIndex: '1', - from: 'A', - to: 'B', - amount: '1', - signer: 'A', - uri: 'firefly://token/8000000000000000000000000000000100000000000000000000000000000001', - data: '', - blockchain: { - id: '000000000001/000000/000001', - name: 'TransferSingle', - location: 'address=0x00001', - signature: transferSingleEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', - from: 'A', - to: 'B', - operator: 'A', - value: '1', - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - signature: transferSingleEventSignature, - }, - }, - }, - }); - return true; - }); - - expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); - }); - - it('Websocket: token approval event', async () => { - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'id=N1&block=1', ''), - }); - - await server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - signature: approvalForAllEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - account: 'A', - approved: true, - operator: 'B', - data: '1', - }, - }, - ]); - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-approval', - data: { - id: '000000000001/000000/000001', - subject: 'A:B', - signer: 'A', - operator: 'B', - poolLocator: 'id=N1&block=1', - approved: true, - data: '', - blockchain: { - id: '000000000001/000000/000001', - name: 'ApprovalForAll', - location: 'address=0x00001', - signature: approvalForAllEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - account: 'A', - approved: true, - operator: 'B', - data: '1', - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - signature: approvalForAllEventSignature, - }, - }, - }, - }); - return true; - }); - }); - - it('Websocket: token transfer event from wrong pool', () => { - const sub = { - name: packSubscriptionName(TOPIC, '0x123', 'id=N1&block=1', ''), - }; - eventstream.getSubscription.mockReturnValueOnce(sub).mockReturnValueOnce(sub); - - return server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - subId: 'sb123', - signature: transferSingleEventSignature, - address: '', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - data: { - id: '340282366920938463463374607431768211456', - from: 'A', - to: 'B', - operator: 'A', - value: '1', - }, - }, - { - subId: 'sb123', - signature: transferSingleEventSignature, - address: '', - blockNumber: '2', - transactionIndex: '0x0', - transactionHash: '0x123', - data: { - id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', - from: 'A', - to: 'B', - operator: 'A', - value: '1', - }, - }, - ]); - }) - .expectJson(message => { - // Only the second transfer should have been processed - expect(message.event).toEqual('token-transfer'); - expect(message.data.poolLocator).toEqual('id=N1&block=1'); - expect(message.data.blockchain.info.blockNumber).toEqual('2'); - return true; - }); - }); - - it('Websocket: token batch transfer', async () => { - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'N1', ''), - }); - - http.get = jest.fn( - () => - new FakeObservable({ - output: 'firefly://token/{id}', - }), - ); - - await server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([ - { - subId: 'sb123', - signature: transferBatchEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - from: 'A', - to: 'B', - operator: 'A', - ids: [ - '57896044618658097711785492504343953926975274699741220483192166611388333031425', - '57896044618658097711785492504343953926975274699741220483192166611388333031426', - ], - values: ['1', '1'], - }, - }, - ]); - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-transfer', - data: { - id: '000000000001/000000/000001/000000', - poolLocator: 'N1', - tokenIndex: '1', - from: 'A', - to: 'B', - amount: '1', - signer: 'A', - uri: 'firefly://token/8000000000000000000000000000000100000000000000000000000000000001', - data: '', - blockchain: { - id: '000000000001/000000/000001', - name: 'TransferBatch', - location: 'address=0x00001', - signature: transferBatchEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - from: 'A', - to: 'B', - operator: 'A', - id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', - value: '1', - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - signature: transferBatchEventSignature, - }, - }, - }, - }); - return true; - }) - .expectJson(message => { - expect(message.id).toBeDefined(); - delete message.id; - expect(message).toEqual({ - event: 'token-transfer', - data: { - id: '000000000001/000000/000001/000001', - poolLocator: 'N1', - tokenIndex: '2', - from: 'A', - to: 'B', - amount: '1', - signer: 'A', - uri: 'firefly://token/8000000000000000000000000000000100000000000000000000000000000002', - data: '', - blockchain: { - id: '000000000001/000000/000001', - name: 'TransferBatch', - location: 'address=0x00001', - signature: transferBatchEventSignature, - timestamp: '2020-01-01 00:00:00Z', - output: { - from: 'A', - to: 'B', - operator: 'A', - id: '57896044618658097711785492504343953926975274699741220483192166611388333031426', - value: '1', - }, - info: { - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - signature: transferBatchEventSignature, - }, - }, - }, - }); - return true; - }); - - expect(http.get).toHaveBeenCalledTimes(1); - expect(http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); - }); - - it('Websocket: success receipt', () => { - return server - .ws('/api/ws') - .exec(() => { - expect(receiptHandler).toBeDefined(); - receiptHandler({ - headers: { - requestId: '1', - type: 'TransactionSuccess', - }, - }); - }) - .expectJson(message => { - expect(message).toEqual({ - event: 'receipt', - data: { - id: '1', - success: true, - }, - }); - return true; - }); - }); - - it('Websocket: error receipt', () => { - return server - .ws('/api/ws') - .exec(() => { - expect(receiptHandler).toBeDefined(); - receiptHandler({ - headers: { - requestId: '1', - type: 'Error', - }, - errorMessage: 'Failed', - }); - }) - .expectJson(message => { - expect(message).toEqual({ - event: 'receipt', - data: { - id: '1', - success: false, - message: 'Failed', - }, - }); - return true; - }); - }); - - it('Websocket: disconnect and reconnect', async () => { - const tokenPoolMessage: TokenCreateEvent = { - subId: 'sb-123', - signature: tokenCreateEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - operator: 'bob', - type_id: '340282366920938463463374607431768211456', - data: '0x6e73006e616d65006964', - }, - }; - - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''), - }); - - await server - .ws('/api/ws') - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([tokenPoolMessage]); - }) - .expectJson(message => { - expect(message.event).toEqual('token-pool'); - return true; - }) - .close(); - - await server.ws('/api/ws').expectJson(message => { - expect(message.event).toEqual('token-pool'); - return true; - }); - }); - - it('Websocket: client switchover', async () => { - const tokenPoolMessage: TokenCreateEvent = { - subId: 'sb-123', - signature: tokenCreateEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - operator: 'bob', - type_id: '340282366920938463463374607431768211456', - data: '0x6e73006e616d65006964', - }, - }; - - eventstream.getSubscription.mockReturnValueOnce({ - name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''), - }); - - const ws1 = server.ws('/api/ws'); - const ws2 = server.ws('/api/ws'); - - await ws1 - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([tokenPoolMessage]); - }) - .expectJson(message => { - expect(message.event).toEqual('token-pool'); - return true; - }) - .close(); - - await ws2.expectJson(message => { - expect(message.event).toEqual('token-pool'); - return true; - }); - }); - - it('Websocket: batch + ack + client switchover', async () => { - const tokenPoolMessage: TokenCreateEvent = { - subId: 'sb-123', - signature: tokenCreateEventSignature, - address: '0x00001', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - operator: 'bob', - type_id: '340282366920938463463374607431768211456', - data: '0x6e73006e616d65006964', - }, - }; - const tokenMintMessage: TransferSingleEvent = { - subId: 'sb-123', - signature: transferSingleEventSignature, - address: '', - blockNumber: '1', - transactionIndex: '0x0', - transactionHash: '0x123', - logIndex: '1', - timestamp: '2020-01-01 00:00:00Z', - data: { - id: '340282366920938463463374607431768211456', - from: ZERO_ADDRESS, - to: 'A', - operator: 'A', - value: '5', - }, - }; - - const sub = { - name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''), - }; - eventstream.getSubscription.mockReturnValueOnce(sub).mockReturnValueOnce(sub); - - const ws1 = server.ws('/api/ws'); - const ws2 = server.ws('/api/ws'); - let messageID1: string; - - await ws1 - .exec(() => { - expect(eventHandler).toBeDefined(); - eventHandler([tokenPoolMessage, tokenMintMessage]); - }) - .expectJson(message => { - expect(message.event).toEqual('token-pool'); - messageID1 = message.id; - return true; - }) - .expectJson(message => { - expect(message.event).toEqual('token-mint'); - return true; - }) - .exec(client => { - client.send( - JSON.stringify({ - event: 'ack', - data: { id: messageID1 }, - }), - ); - }) - .close(); - - await ws2.expectJson(message => { - expect(message.event).toEqual('token-mint'); - return true; - }); - }); + addSuite('ERC1155 API', SuiteApi); + addSuite('Websocket Events', SuiteWebsocket); }); diff --git a/test/suites/api.ts b/test/suites/api.ts new file mode 100644 index 0000000..82b1ca4 --- /dev/null +++ b/test/suites/api.ts @@ -0,0 +1,280 @@ +import { + TokenPool, + TokenType, + EthConnectAsyncResponse, + TokenMint, + TokenBurn, + TokenTransfer, + TokenApproval, + TokenBalanceQuery, + EthConnectReturn, + TokenBalance, +} from '../../src/tokens/tokens.interfaces'; +import { TestContext, FakeObservable, BASE_URL, INSTANCE_PATH } from '../app.e2e-context'; + +const IDENTITY = '0x1'; +const OPTIONS = { + params: { + 'fly-from': IDENTITY, + 'fly-sync': 'false', + }, +}; + +export default (context: TestContext) => { + it('Create fungible pool', async () => { + const request: TokenPool = { + type: TokenType.FUNGIBLE, + requestId: 'op1', + data: 'tx1', + signer: IDENTITY, + }; + const response: EthConnectAsyncResponse = { + id: 'op1', + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/createpool').send(request).expect(202).expect({ id: 'op1' }); + + expect(context.http.post).toHaveBeenCalledTimes(1); + expect(context.http.post).toHaveBeenCalledWith( + `${BASE_URL}${INSTANCE_PATH}/create`, + { + data: '0x747831', + is_fungible: true, + }, + { + ...OPTIONS, + params: { + ...OPTIONS.params, + 'fly-id': 'op1', + }, + }, + ); + }); + + it('Create non-fungible pool', async () => { + const request: TokenPool = { + type: TokenType.NONFUNGIBLE, + signer: '0xabc', + }; + const response: EthConnectAsyncResponse = { + id: '1', + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/createpool').send(request).expect(202).expect({ id: '1' }); + + expect(context.http.post).toHaveBeenCalledTimes(1); + expect(context.http.post).toHaveBeenCalledWith( + `${BASE_URL}${INSTANCE_PATH}/create`, + { + data: '0x00', + is_fungible: false, + }, + { + ...OPTIONS, + params: { + ...OPTIONS.params, + 'fly-from': '0xabc', + }, + }, + ); + }); + + it('Create pool - unrecognized fields', async () => { + const request = { + type: TokenType.FUNGIBLE, + signer: IDENTITY, + isBestPool: true, // will be stripped but will not cause an error + }; + const response: EthConnectAsyncResponse = { + id: 'op1', + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/createpool').send(request).expect(202).expect({ id: 'op1' }); + }); + + it('Mint fungible token', async () => { + const request: TokenMint = { + poolLocator: 'F1', + to: '1', + amount: '2', + data: 'test', + signer: IDENTITY, + }; + const response: EthConnectAsyncResponse = { + id: '1', + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/mint').send(request).expect(202).expect({ id: '1' }); + + expect(context.http.post).toHaveBeenCalledTimes(1); + expect(context.http.post).toHaveBeenCalledWith( + `${BASE_URL}${INSTANCE_PATH}/mintFungible`, + { + type_id: '340282366920938463463374607431768211456', + to: ['1'], + amounts: ['2'], + data: '0x74657374', + }, + OPTIONS, + ); + }); + + it('Mint non-fungible token', async () => { + const request: TokenMint = { + poolLocator: 'N1', + to: '1', + amount: '2', + signer: IDENTITY, + }; + const response: EthConnectAsyncResponse = { + id: '1', + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/mint').send(request).expect(202).expect({ id: '1' }); + + expect(context.http.post).toHaveBeenCalledTimes(1); + expect(context.http.post).toHaveBeenCalledWith( + `${BASE_URL}${INSTANCE_PATH}/mintNonFungible`, + { + type_id: '57896044618658097711785492504343953926975274699741220483192166611388333031424', + to: ['1', '1'], + data: '0x00', + }, + OPTIONS, + ); + }); + + it('Burn token', async () => { + const request: TokenBurn = { + poolLocator: 'N1', + tokenIndex: '1', + from: 'A', + amount: '1', + data: 'tx1', + signer: IDENTITY, + }; + const response: EthConnectAsyncResponse = { + id: '1', + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/burn').send(request).expect(202).expect({ id: '1' }); + + expect(context.http.post).toHaveBeenCalledTimes(1); + expect(context.http.post).toHaveBeenCalledWith( + `${BASE_URL}${INSTANCE_PATH}/burn`, + { + id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', + from: 'A', + amount: '1', + data: '0x747831', + }, + OPTIONS, + ); + }); + + it('Transfer token', async () => { + const request: TokenTransfer = { + poolLocator: 'F1', + from: '1', + to: '2', + amount: '2', + signer: IDENTITY, + }; + const response: EthConnectAsyncResponse = { + id: '1', + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/transfer').send(request).expect(202).expect({ id: '1' }); + + expect(context.http.post).toHaveBeenCalledTimes(1); + expect(context.http.post).toHaveBeenCalledWith( + `${BASE_URL}${INSTANCE_PATH}/safeTransferFrom`, + { + id: '340282366920938463463374607431768211456', + from: '1', + to: '2', + amount: '2', + data: '0x00', + }, + OPTIONS, + ); + }); + + it('Token approval', async () => { + const request: TokenApproval = { + poolLocator: 'F1', + signer: IDENTITY, + operator: '2', + approved: true, + }; + const response: EthConnectAsyncResponse = { + id: '1', + sent: true, + }; + + context.http.post = jest.fn(() => new FakeObservable(response)); + + await context.server.post('/approval').send(request).expect(202).expect({ id: '1' }); + + expect(context.http.post).toHaveBeenCalledTimes(1); + expect(context.http.post).toHaveBeenCalledWith( + `${BASE_URL}${INSTANCE_PATH}/setApprovalForAllWithData`, + { + operator: '2', + approved: true, + data: '0x00', + }, + OPTIONS, + ); + }); + + it('Query balance', async () => { + const request: TokenBalanceQuery = { + account: '1', + poolLocator: 'F1', + tokenIndex: '0', + }; + const response: EthConnectReturn = { + output: '1', + }; + + context.http.get = jest.fn(() => new FakeObservable(response)); + + await context.server + .get('/balance') + .query(request) + .expect(200) + .expect({ + balance: '1', + }); + + expect(context.http.get).toHaveBeenCalledTimes(1); + expect(context.http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/balanceOf`, { + params: { + account: '1', + id: '340282366920938463463374607431768211456', + }, + }); + }); +}; diff --git a/test/suites/websocket.ts b/test/suites/websocket.ts new file mode 100644 index 0000000..5b8d237 --- /dev/null +++ b/test/suites/websocket.ts @@ -0,0 +1,994 @@ +// Copyright © 2021 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + EventStreamReply, + EventStreamSubscription, +} from '../../src/event-stream/event-stream.interfaces'; +import { ReceiptEvent } from '../../src/eventstream-proxy/eventstream-proxy.interfaces'; +import { + ApprovalForAllEvent, + EthConnectReturn, + TokenApprovalEvent, + TokenBurnEvent, + TokenCreateEvent, + TokenMintEvent, + TokenPoolEvent, + TokenTransferEvent, + TransferBatchEvent, + TransferSingleEvent, +} from '../../src/tokens/tokens.interfaces'; +import { WebSocketMessage } from '../../src/websocket-events/websocket-events.base'; +import { packSubscriptionName } from '../../src/tokens/tokens.util'; +import { BASE_URL, FakeObservable, INSTANCE_PATH, TestContext, TOPIC } from '../app.e2e-context'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +const tokenCreateEventSignature = 'TokenCreate(address,uint256,bytes)'; +const transferSingleEventSignature = 'TransferSingle(address,address,address,uint256,uint256)'; +const approvalForAllEventSignature = 'ApprovalForAll(address,address,bool)'; +const transferBatchEventSignature = 'TransferBatch(address,address,address,uint256[],uint256[])'; + +export default (context: TestContext) => { + it('Token pool event', () => { + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'F1', ''), + }); + + return context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + subId: 'sb123', + signature: tokenCreateEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + timestamp: '2020-01-01 00:00:00Z', + data: { + operator: 'bob', + type_id: '340282366920938463463374607431768211456', + data: '0x00', + }, + }, + ]); + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-pool', + data: { + standard: 'ERC1155', + poolLocator: 'id=F1&block=1', + type: 'fungible', + signer: 'bob', + data: '', + info: { + address: '0x00001', + typeId: '0x0000000000000000000000000000000100000000000000000000000000000000', + }, + blockchain: { + id: '000000000001/000000/000000', + name: 'TokenCreate', + location: 'address=0x00001', + signature: tokenCreateEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + operator: 'bob', + type_id: '340282366920938463463374607431768211456', + data: '0x00', + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + signature: tokenCreateEventSignature, + }, + }, + }, + }); + return true; + }); + }); + + it('Token pool event from base subscription', () => { + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'base', ''), + }); + + return context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + subId: 'sb123', + signature: tokenCreateEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + timestamp: '2020-01-01 00:00:00Z', + data: { + operator: 'bob', + type_id: '340282366920938463463374607431768211456', + data: '0x00', + }, + }, + ]); + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-pool', + data: { + standard: 'ERC1155', + poolLocator: 'id=F1&block=1', + type: 'fungible', + signer: 'bob', + data: '', + info: { + address: '0x00001', + typeId: '0x0000000000000000000000000000000100000000000000000000000000000000', + }, + blockchain: { + id: '000000000001/000000/000000', + name: 'TokenCreate', + location: 'address=0x00001', + signature: tokenCreateEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + operator: 'bob', + type_id: '340282366920938463463374607431768211456', + data: '0x00', + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + signature: tokenCreateEventSignature, + }, + }, + }, + }); + return true; + }); + }); + + it('Token mint event', async () => { + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''), + }); + + context.http.get = jest.fn( + () => + new FakeObservable({ + output: 'firefly://token/{id}', + }), + ); + + await context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + subId: 'sb-123', + signature: transferSingleEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + id: '340282366920938463463374607431768211456', + from: ZERO_ADDRESS, + to: 'A', + operator: 'A', + value: '5', + transaction: { + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + }, + }, + inputMethod: 'mintFungible', + inputArgs: { + data: '0x74657374', + }, + }, + ]); + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-mint', + data: { + id: '000000000001/000000/000001', + poolLocator: 'id=F1&block=1', + to: 'A', + amount: '5', + signer: 'A', + uri: 'firefly://token/0000000000000000000000000000000100000000000000000000000000000000', + data: 'test', + blockchain: { + id: '000000000001/000000/000001', + name: 'TransferSingle', + location: 'address=0x00001', + signature: transferSingleEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + id: '340282366920938463463374607431768211456', + from: ZERO_ADDRESS, + to: 'A', + operator: 'A', + value: '5', + transaction: { + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + }, + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + signature: transferSingleEventSignature, + }, + }, + }, + }); + return true; + }); + + expect(context.http.get).toHaveBeenCalledTimes(1); + expect(context.http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); + }); + + it('Token mint event with old pool ID', async () => { + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'F1', ''), + }); + + context.http.get = jest.fn( + () => + new FakeObservable({ + output: 'firefly://token/{id}', + }), + ); + + await context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + subId: 'sb-123', + signature: transferSingleEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + id: '340282366920938463463374607431768211456', + from: ZERO_ADDRESS, + to: 'A', + operator: 'A', + value: '5', + transaction: { + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + }, + }, + inputMethod: 'mintFungible', + inputArgs: { + data: '0x74657374', + }, + }, + ]); + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-mint', + data: { + id: '000000000001/000000/000001', + poolLocator: 'F1', + to: 'A', + amount: '5', + signer: 'A', + uri: 'firefly://token/0000000000000000000000000000000100000000000000000000000000000000', + data: 'test', + blockchain: { + id: '000000000001/000000/000001', + name: 'TransferSingle', + location: 'address=0x00001', + signature: transferSingleEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + id: '340282366920938463463374607431768211456', + from: ZERO_ADDRESS, + to: 'A', + operator: 'A', + value: '5', + transaction: { + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + }, + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + signature: transferSingleEventSignature, + }, + }, + }, + }); + return true; + }); + + expect(context.http.get).toHaveBeenCalledTimes(1); + expect(context.http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); + }); + + it('Token burn event', async () => { + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'id=N1&block=1', ''), + }); + + context.http.get = jest.fn( + () => + new FakeObservable({ + output: 'firefly://token/{id}', + }), + ); + + await context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + subId: 'sb-123', + signature: transferSingleEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', + from: 'A', + to: ZERO_ADDRESS, + operator: 'A', + value: '1', + transaction: { + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + }, + }, + inputMethod: 'burn', + inputArgs: { + data: '0x74657374', + }, + }, + ]); + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-burn', + data: { + id: '000000000001/000000/000001', + poolLocator: 'id=N1&block=1', + tokenIndex: '1', + from: 'A', + amount: '1', + signer: 'A', + uri: 'firefly://token/8000000000000000000000000000000100000000000000000000000000000001', + data: 'test', + blockchain: { + id: '000000000001/000000/000001', + name: 'TransferSingle', + location: 'address=0x00001', + signature: transferSingleEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', + from: 'A', + to: ZERO_ADDRESS, + operator: 'A', + value: '1', + transaction: { + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + }, + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + signature: transferSingleEventSignature, + }, + }, + }, + }); + return true; + }); + + expect(context.http.get).toHaveBeenCalledTimes(1); + expect(context.http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); + }); + + it('Token transfer event', async () => { + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'id=N1&block=1', ''), + }); + + context.http.get = jest.fn( + () => + new FakeObservable({ + output: 'firefly://token/{id}', + }), + ); + + await context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + subId: 'sb123', + signature: transferSingleEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', + from: 'A', + to: 'B', + operator: 'A', + value: '1', + }, + }, + ]); + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-transfer', + data: { + id: '000000000001/000000/000001', + poolLocator: 'id=N1&block=1', + tokenIndex: '1', + from: 'A', + to: 'B', + amount: '1', + signer: 'A', + uri: 'firefly://token/8000000000000000000000000000000100000000000000000000000000000001', + data: '', + blockchain: { + id: '000000000001/000000/000001', + name: 'TransferSingle', + location: 'address=0x00001', + signature: transferSingleEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', + from: 'A', + to: 'B', + operator: 'A', + value: '1', + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + signature: transferSingleEventSignature, + }, + }, + }, + }); + return true; + }); + + expect(context.http.get).toHaveBeenCalledTimes(1); + expect(context.http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); + }); + + it('Token approval event', async () => { + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'id=N1&block=1', ''), + }); + + await context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + signature: approvalForAllEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + account: 'A', + approved: true, + operator: 'B', + data: '1', + }, + }, + ]); + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-approval', + data: { + id: '000000000001/000000/000001', + subject: 'A:B', + signer: 'A', + operator: 'B', + poolLocator: 'id=N1&block=1', + approved: true, + data: '', + blockchain: { + id: '000000000001/000000/000001', + name: 'ApprovalForAll', + location: 'address=0x00001', + signature: approvalForAllEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + account: 'A', + approved: true, + operator: 'B', + data: '1', + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + signature: approvalForAllEventSignature, + }, + }, + }, + }); + return true; + }); + }); + + it('Token transfer event from wrong pool', () => { + const sub = { + name: packSubscriptionName(TOPIC, '0x123', 'id=N1&block=1', ''), + }; + context.eventstream.getSubscription.mockReturnValueOnce(sub).mockReturnValueOnce(sub); + + return context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + subId: 'sb123', + signature: transferSingleEventSignature, + address: '', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + data: { + id: '340282366920938463463374607431768211456', + from: 'A', + to: 'B', + operator: 'A', + value: '1', + }, + }, + { + subId: 'sb123', + signature: transferSingleEventSignature, + address: '', + blockNumber: '2', + transactionIndex: '0x0', + transactionHash: '0x123', + data: { + id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', + from: 'A', + to: 'B', + operator: 'A', + value: '1', + }, + }, + ]); + }) + .expectJson(message => { + // Only the second transfer should have been processed + expect(message.event).toEqual('token-transfer'); + expect(message.data.poolLocator).toEqual('id=N1&block=1'); + expect(message.data.blockchain.info.blockNumber).toEqual('2'); + return true; + }); + }); + + it('Token batch transfer', async () => { + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'N1', ''), + }); + + context.http.get = jest.fn( + () => + new FakeObservable({ + output: 'firefly://token/{id}', + }), + ); + + await context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([ + { + subId: 'sb123', + signature: transferBatchEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + from: 'A', + to: 'B', + operator: 'A', + ids: [ + '57896044618658097711785492504343953926975274699741220483192166611388333031425', + '57896044618658097711785492504343953926975274699741220483192166611388333031426', + ], + values: ['1', '1'], + }, + }, + ]); + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-transfer', + data: { + id: '000000000001/000000/000001/000000', + poolLocator: 'N1', + tokenIndex: '1', + from: 'A', + to: 'B', + amount: '1', + signer: 'A', + uri: 'firefly://token/8000000000000000000000000000000100000000000000000000000000000001', + data: '', + blockchain: { + id: '000000000001/000000/000001', + name: 'TransferBatch', + location: 'address=0x00001', + signature: transferBatchEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + from: 'A', + to: 'B', + operator: 'A', + id: '57896044618658097711785492504343953926975274699741220483192166611388333031425', + value: '1', + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + signature: transferBatchEventSignature, + }, + }, + }, + }); + return true; + }) + .expectJson(message => { + expect(message.id).toBeDefined(); + delete message.id; + expect(message).toEqual({ + event: 'token-transfer', + data: { + id: '000000000001/000000/000001/000001', + poolLocator: 'N1', + tokenIndex: '2', + from: 'A', + to: 'B', + amount: '1', + signer: 'A', + uri: 'firefly://token/8000000000000000000000000000000100000000000000000000000000000002', + data: '', + blockchain: { + id: '000000000001/000000/000001', + name: 'TransferBatch', + location: 'address=0x00001', + signature: transferBatchEventSignature, + timestamp: '2020-01-01 00:00:00Z', + output: { + from: 'A', + to: 'B', + operator: 'A', + id: '57896044618658097711785492504343953926975274699741220483192166611388333031426', + value: '1', + }, + info: { + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + signature: transferBatchEventSignature, + }, + }, + }, + }); + return true; + }); + + expect(context.http.get).toHaveBeenCalledTimes(1); + expect(context.http.get).toHaveBeenCalledWith(`${BASE_URL}${INSTANCE_PATH}/uri?input=0`, {}); + }); + + it('Success receipt', () => { + return context.server + .ws('/api/ws') + .exec(() => { + expect(context.receiptHandler).toBeDefined(); + context.receiptHandler({ + headers: { + requestId: '1', + type: 'TransactionSuccess', + }, + }); + }) + .expectJson(message => { + expect(message).toEqual({ + event: 'receipt', + data: { + id: '1', + success: true, + }, + }); + return true; + }); + }); + + it('Error receipt', () => { + return context.server + .ws('/api/ws') + .exec(() => { + expect(context.receiptHandler).toBeDefined(); + context.receiptHandler({ + headers: { + requestId: '1', + type: 'Error', + }, + errorMessage: 'Failed', + }); + }) + .expectJson(message => { + expect(message).toEqual({ + event: 'receipt', + data: { + id: '1', + success: false, + message: 'Failed', + }, + }); + return true; + }); + }); + + it('Disconnect and reconnect', async () => { + const tokenPoolMessage: TokenCreateEvent = { + subId: 'sb-123', + signature: tokenCreateEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + operator: 'bob', + type_id: '340282366920938463463374607431768211456', + data: '0x6e73006e616d65006964', + }, + }; + + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''), + }); + + await context.server + .ws('/api/ws') + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([tokenPoolMessage]); + }) + .expectJson(message => { + expect(message.event).toEqual('token-pool'); + return true; + }) + .close(); + + await context.server.ws('/api/ws').expectJson(message => { + expect(message.event).toEqual('token-pool'); + return true; + }); + }); + + it('Client switchover', async () => { + const tokenPoolMessage: TokenCreateEvent = { + subId: 'sb-123', + signature: tokenCreateEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + operator: 'bob', + type_id: '340282366920938463463374607431768211456', + data: '0x6e73006e616d65006964', + }, + }; + + context.eventstream.getSubscription.mockReturnValueOnce({ + name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''), + }); + + const ws1 = context.server.ws('/api/ws'); + const ws2 = context.server.ws('/api/ws'); + + await ws1 + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([tokenPoolMessage]); + }) + .expectJson(message => { + expect(message.event).toEqual('token-pool'); + return true; + }) + .close(); + + await ws2.expectJson(message => { + expect(message.event).toEqual('token-pool'); + return true; + }); + }); + + it('Batch + ack + client switchover', async () => { + const tokenPoolMessage: TokenCreateEvent = { + subId: 'sb-123', + signature: tokenCreateEventSignature, + address: '0x00001', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + operator: 'bob', + type_id: '340282366920938463463374607431768211456', + data: '0x6e73006e616d65006964', + }, + }; + const tokenMintMessage: TransferSingleEvent = { + subId: 'sb-123', + signature: transferSingleEventSignature, + address: '', + blockNumber: '1', + transactionIndex: '0x0', + transactionHash: '0x123', + logIndex: '1', + timestamp: '2020-01-01 00:00:00Z', + data: { + id: '340282366920938463463374607431768211456', + from: ZERO_ADDRESS, + to: 'A', + operator: 'A', + value: '5', + }, + }; + + const sub = { + name: packSubscriptionName(TOPIC, '0x123', 'id=F1&block=1', ''), + }; + context.eventstream.getSubscription.mockReturnValueOnce(sub).mockReturnValueOnce(sub); + + const ws1 = context.server.ws('/api/ws'); + const ws2 = context.server.ws('/api/ws'); + let messageID1: string; + + await ws1 + .exec(() => { + expect(context.eventHandler).toBeDefined(); + context.eventHandler([tokenPoolMessage, tokenMintMessage]); + }) + .expectJson(message => { + expect(message.event).toEqual('token-pool'); + messageID1 = message.id; + return true; + }) + .expectJson(message => { + expect(message.event).toEqual('token-mint'); + return true; + }) + .exec(client => { + client.send( + JSON.stringify({ + event: 'ack', + data: { id: messageID1 }, + }), + ); + }) + .close(); + + await ws2.expectJson(message => { + expect(message.event).toEqual('token-mint'); + return true; + }); + }); +};