diff --git a/.github/workflows/check-sdk-version.yaml b/.github/workflows/check-sdk-version.yaml new file mode 100644 index 0000000..771ad1b --- /dev/null +++ b/.github/workflows/check-sdk-version.yaml @@ -0,0 +1,20 @@ +name: Test - Check SDK version + +on: + pull_request + +jobs: + check-sdk-version: + runs-on: ubuntu-20.04 + + steps: + - name: 🏗 Setup repo + uses: actions/checkout@v3 + + - name: 📦 Check it is using the latest version of the SDK + uses: actions/setup-node@v3 + with: + node-version: 18.12.1 + - run: corepack enable + - run: yarn install --immutable + - run: yarn run updateSdkVersion $GITHUB_BASE_REF check \ No newline at end of file diff --git a/.github/workflows/master-pr-tests.yaml b/.github/workflows/master-pr-tests.yaml deleted file mode 100644 index 04b04bc..0000000 --- a/.github/workflows/master-pr-tests.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Test - Master branch PR - -on: - pull_request: - branches: - - master - # triggers when a PR with target to master is opened - -jobs: - check-sdk-version: - runs-on: ubuntu-20.04 - - steps: - - name: 🏗 Setup repo - uses: actions/checkout@v3 - - - name: 📦 Check it is using the latest version of the SDK - uses: actions/setup-node@v3 - with: - node-version: 18.12.1 - - run: corepack enable - - run: echo "YARNLOCK_SHA256_OLD=$(sha256sum yarn.lock)" >> $GITHUB_ENV - - run: yarn up @tonomy/tonomy-id-sdk - - run: echo "YARNLOCK_SHA256_NEW=$(sha256sum yarn.lock)" >> $GITHUB_ENV - - run: if [ "$YARNLOCK_SHA256_NEW" != "$YARNLOCK_SHA256_OLD" ]; then exit 1; fi - # if this step fails, make sure the Tonomy SDK is set to the latest package - # try `yarn up @tonomy/tonomy-id-sdk` and - # then test locally, if it works, then commit changes to package.json and yarn.lock \ No newline at end of file diff --git a/README.md b/README.md index f69bf6d..296018f 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,6 @@ yarn install ``` -For the `development` branch - -```bash -yarn add @tonomy/tonomy-id-sdk@development -``` - ## Running the app ```bash @@ -29,6 +23,14 @@ yarn run start:dev yarn run start:prod ``` +## Update the Tonomy-ID-SDK version to the latest + +```bash +yarn run updateSdkVersion development +# or +yarn run updateSdkVersion master +``` + ## Environment variables and configuration `NODE_ENV` - Determines which config file in `./src/config` to use diff --git a/package.json b/package.json index 65ca0c2..e7c9d08 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "updateSdkVersion": "./update_sdk_version.sh" }, "dependencies": { "@nestjs/common": "^10.2.3", @@ -27,7 +28,7 @@ "@nestjs/platform-socket.io": "^10.2.3", "@nestjs/swagger": "^7.1.10", "@nestjs/websockets": "^10.2.3", - "@tonomy/tonomy-id-sdk": "^0.15.0-development.2", + "@tonomy/tonomy-id-sdk": "0.15.0-development.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "hcaptcha": "^0.1.1", diff --git a/src/communication/communication.gateway.ts b/src/communication/communication.gateway.ts index 9562098..70e0c95 100644 --- a/src/communication/communication.gateway.ts +++ b/src/communication/communication.gateway.ts @@ -16,11 +16,17 @@ import { UsePipes, } from '@nestjs/common'; import { TransformVcPipe } from './transform-vc/transform-vc.pipe'; -import { MessageDto, MessageRto } from './dto/message.dto'; import { Client } from './dto/client.dto'; import { WsExceptionFilter } from './ws-exception/ws-exception.filter'; import { AuthenticationMessage } from '@tonomy/tonomy-id-sdk'; import { CommunicationGuard } from './communication.guard'; +import { BodyDto } from './dto/body.dto'; + +export type WebsocketReturnType = { + status: HttpStatus; + details?: any; + error?: any; +}; @UseFilters(WsExceptionFilter) @UsePipes(new TransformVcPipe()) @@ -39,38 +45,58 @@ export class CommunicationGateway implements OnGatewayDisconnect { /** * Logs in the user and added it to the loggedIn map * - * @param {MessageDto} message - the VC the user sent + * @param {BodyDto} body - The message VC or an error from the transformer * @param {Client} client - user socket - * @returns void */ @SubscribeMessage('login') - connectUser( - @MessageBody() message: MessageDto, + async connectUser( + @MessageBody() body: BodyDto, @ConnectedSocket() client: Client, ) { - if (message.getType() !== AuthenticationMessage.getType()) { - throw new HttpException( - "Message type must be 'AuthenticationMessage'", - HttpStatus.BAD_REQUEST, - ); - } + try { + if (body.error) throw body.error; + if (!body.value) throw new Error('Body not found'); + const message = body.value; + + if (message.getType() !== AuthenticationMessage.getType()) { + throw new HttpException( + "Message type must be 'AuthenticationMessage'", + HttpStatus.BAD_REQUEST, + ); + } - return this.usersService.login(message.getSender(), client); + return { + status: HttpStatus.OK, + details: await this.usersService.login(message.getSender(), client), + }; + } catch (e) { + return this.usersService.handleError(e); + } } /** * sends the message to the VC recipient if is connected and loggedIn - * @param message the VC the user sent + * @param {BodyDto} body - The message VC or an error from the transformer * @param client user socket - * @returns void */ @SubscribeMessage('message') @UseGuards(CommunicationGuard) - relayMessage( - @MessageBody() message: MessageDto, + async relayMessage( + @MessageBody() body: BodyDto, @ConnectedSocket() client: Client, ) { - return this.usersService.sendMessage(client, message); + try { + if (body.error) throw body.error; + if (!body.value) throw new Error('Body not found'); + const message = body.value; + + return { + status: HttpStatus.OK, + details: await this.usersService.sendMessage(client, message), + }; + } catch (e) { + return this.usersService.handleError(e); + } } /** diff --git a/src/communication/communication.service.ts b/src/communication/communication.service.ts index 6b5d1ee..a77ab90 100644 --- a/src/communication/communication.service.ts +++ b/src/communication/communication.service.ts @@ -3,6 +3,7 @@ import { Socket } from 'socket.io'; import { Client } from './dto/client.dto'; import { MessageDto } from './dto/message.dto'; import settings from '../settings'; +import { WebsocketReturnType } from './communication.gateway'; @Injectable() export class CommunicationService { @@ -67,4 +68,21 @@ export class CommunicationService { return true; } + + handleError(e): WebsocketReturnType { + if (settings.env !== 'test') console.error(e); + // this.logger.error(e); // This does not print stack trace + + if (e instanceof HttpException) { + return { + status: e.getStatus(), + error: e.getResponse(), + }; + } + + return { + status: HttpStatus.INTERNAL_SERVER_ERROR, + error: e.message, + }; + } } diff --git a/src/communication/dto/body.dto.ts b/src/communication/dto/body.dto.ts new file mode 100644 index 0000000..8eefb2f --- /dev/null +++ b/src/communication/dto/body.dto.ts @@ -0,0 +1,6 @@ +import { MessageDto } from './message.dto'; + +export type BodyDto = { + value?: MessageDto; + error?: any; +}; diff --git a/src/communication/transform-vc/transform-vc.pipe.ts b/src/communication/transform-vc/transform-vc.pipe.ts index ff2307f..55b5c58 100644 --- a/src/communication/transform-vc/transform-vc.pipe.ts +++ b/src/communication/transform-vc/transform-vc.pipe.ts @@ -6,42 +6,53 @@ import { PipeTransform, } from '@nestjs/common'; import { MessageDto, MessageRto } from '../dto/message.dto'; +import { BodyDto } from '../dto/body.dto'; @Injectable() export class TransformVcPipe implements PipeTransform { - async transform(value: MessageRto, metadata: ArgumentMetadata) { - if (metadata.type === 'body') { - const message = new MessageDto(value.message); + async transform( + value: MessageRto, + metadata: ArgumentMetadata, + ): Promise { + try { + if (metadata.type === 'body') { + const message = new MessageDto(value.message); - try { - const result = await message.verify(); + try { + const result = await message.verify(); - if (!result) - throw new HttpException( - `VC not could not verify signer from ${message.getSender()}`, - HttpStatus.UNAUTHORIZED, - ); - return message; - } catch (e) { - if ( - e.message?.startsWith( - 'resolver_error: Unable to resolve DID document for', - ) - ) { - throw new HttpException( - `DID could not be resolved from ${message.getSender()}`, - HttpStatus.NOT_FOUND, - ); - } + if (!result) + throw new HttpException( + `VC not could not verify signer from ${message.getSender()}`, + HttpStatus.UNAUTHORIZED, + ); + return { value: message }; + } catch (e) { + if ( + e.message?.startsWith( + 'resolver_error: Unable to resolve DID document for', + ) + ) { + throw new HttpException( + `DID could not be resolved from ${message.getSender()}`, + HttpStatus.NOT_FOUND, + ); + } - if (e instanceof HttpException) { - throw e; - } + if (e instanceof HttpException) { + throw e; + } - throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR); + throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR); + } + } else { + // Do nothing. transform() is not needed + return value; } + } catch (e) { + return { + error: e, + }; } - - return value; } } diff --git a/src/settings.ts b/src/settings.ts index 8a34eda..4b4374f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -5,7 +5,7 @@ import { EosioUtil } from '@tonomy/tonomy-id-sdk'; const env = process.env.NODE_ENV || 'development'; -console.log(`NODE_ENV=${env}`); +if (env !== 'test') console.log(`NODE_ENV=${env}`); type ConfigType = { blockchainUrl: string; @@ -74,7 +74,7 @@ if (process.env.BLOCKCHAIN_URL) { settings.config.blockchainUrl = process.env.BLOCKCHAIN_URL; } -console.log('settings', settings); +if (env !== 'test') console.log('settings', settings); settings.secrets = { createAccountPrivateKey: EosioUtil.defaultAntelopePrivateKey.toString(), diff --git a/test/communication.e2e-spec.ts b/test/communication.e2e-spec.ts new file mode 100644 index 0000000..12f3882 --- /dev/null +++ b/test/communication.e2e-spec.ts @@ -0,0 +1,68 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { AppModule } from '../src/app.module'; +import { connectSocket, emitMessage } from './ws-client.helper'; +import { Socket } from 'socket.io-client'; +import { + AuthenticationMessage, + ES256KSigner, + generateRandomKeyPair, + setSettings, +} from '@tonomy/tonomy-id-sdk'; +// @ts-expect-error - cannot find module or its corresponding type declarations +import { createJWK, toDid } from '@tonomy/tonomy-id-sdk/util'; + +setSettings({ + blockchainUrl: 'http://localhost:8888', +}); + +describe('CommunicationGateway (e2e)', () => { + let app: INestApplication; + let socket: Socket; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.listen(5000); + + socket = await connectSocket(); + }); + + afterEach(async () => { + await socket.disconnect(); + await socket.close(); + await app.close(); + }); + + describe('login event', () => { + it('fails when provide an empty body', async () => { + await expect(() => emitMessage(socket, 'login', {})).rejects.toThrow( + "Cannot read properties of undefined (reading 'getCredentialSubject')", + ); + }); + + it('succeeds for did:jwk message', async () => { + const { privateKey, publicKey } = generateRandomKeyPair(); + const signer = ES256KSigner(privateKey.data.array, true); + const jwk = await createJWK(publicKey); + const did = toDid(jwk); + + const issuer = { + did, + signer, + alg: 'ES256K-R', + }; + const message = await AuthenticationMessage.signMessageWithoutRecipient( + { data: 'test' }, + issuer, + ); + + const response = await emitMessage(socket, 'login', { message }); + + expect(response).toBeTruthy(); + }); + }); +}); diff --git a/test/ws-client.helper.ts b/test/ws-client.helper.ts new file mode 100644 index 0000000..bb1fa55 --- /dev/null +++ b/test/ws-client.helper.ts @@ -0,0 +1,57 @@ +import { io, Socket } from 'socket.io-client'; +import { HttpStatus } from '@nestjs/common'; +import { WebsocketReturnType } from 'src/communication/communication.gateway'; + +export const SOCKET_TIMEOUT = 1000; + +export async function connectSocket(): Promise { + const socket = io('http://localhost:5000', { + autoConnect: false, + transports: ['websocket'], + }); + + socket.connect(); + let resolved = false; + + await new Promise((resolve, reject) => { + socket.on('connect', () => { + resolved = true; + resolve(); + }); + + setTimeout(() => { + if (resolved) return; + reject(new Error('Websocket connection event timed out')); + }, SOCKET_TIMEOUT); + }); + return socket; +} + +export async function emitMessage( + socket: Socket, + event: string, + body: any, +): Promise { + const res: WebsocketReturnType = await new Promise((resolve, reject) => { + socket + .timeout(SOCKET_TIMEOUT) + .emit(event, body, (error: any, response: any) => { + if (error) { + reject(error); + return; + } + + resolve(response); + }); + }); + + if (res.status !== HttpStatus.OK) { + const err = new Error(res.error); + + // @ts-expect-error - code is not a property of Error + err.code = res.status; + throw err; + } else { + return res.details; + } +} diff --git a/update_sdk_version.sh b/update_sdk_version.sh new file mode 100755 index 0000000..6e1ec00 --- /dev/null +++ b/update_sdk_version.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +BRANCH=${1-default} +CHECK=${2-default} + +echo "yarn run updateSdkVersion $@" + +set +e + +# Get the latest version of the SDK for the correct npmjs tag based on branch +if [ "${BRANCH}" == "master" ]; then + VERSION=$(npm view @tonomy/tonomy-id-sdk version) +elif [ "${BRANCH}" == "development" ]; then + VERSION=$(npm view @tonomy/tonomy-id-sdk@development version) +else + # Print help + echo "Usage: yarn run updateSdkVersion master|development [check]" + echo "" + echo "Example: yarn run updateSdkVersion development" + echo "Example: yarn run updateSdkVersion master check" + echo "" + exit 1 +fi + +# Get the sha256sum of yarn.lock before updating the SDK +YARNLOCK_SHA256_OLD=$(sha256sum yarn.lock) + +# Update the SDK +echo "Updating package @tonomy/tonomy-id-sdk@${VERSION}" +yarn up "@tonomy/tonomy-id-sdk@${VERSION}" + +# Get the sha256sum of yarn.lock after updating the SDK +YARNLOCK_SHA256_NEW=$(sha256sum yarn.lock) + +if [ "${CHECK}" == "check" ]; then + echo "Checking if yarn.lock has changed" + if [ "${YARNLOCK_SHA256_NEW}" != "${YARNLOCK_SHA256_OLD}" ]; then + echo "yarn.lock has changed while updating @tonomy/tonomy-id-sdk@${VERSION}" + echo "Please run \"yarn run installSdkVersion\" to fix" + exit 1 + fi +fi \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3cf2184..783910a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1576,9 +1576,9 @@ __metadata: languageName: node linkType: hard -"@tonomy/tonomy-id-sdk@npm:^0.15.0-development.2": - version: 0.15.0-development.2 - resolution: "@tonomy/tonomy-id-sdk@npm:0.15.0-development.2" +"@tonomy/tonomy-id-sdk@npm:0.15.0-development.3": + version: 0.15.0-development.3 + resolution: "@tonomy/tonomy-id-sdk@npm:0.15.0-development.3" dependencies: "@consento/sync-randombytes": ^1.0.5 "@tonomy/antelope-did": ^0.1.5 @@ -1593,7 +1593,7 @@ __metadata: elliptic: ^6.5.4 socket.io-client: ^4.5.4 universal-base64url: ^1.1.0 - checksum: 33d6031035c480da14ad603a7bb4e94ffcc6138677ae9fcf301f962a96328a449ee636c7fdec7825bda79853b93fc9781586da62eae341763c6686c3b58afcdc + checksum: 6ddbf3175f95f994fdba13d9d99d2ae9454b54a8bc264bb754c59fd3aa40460a6856fe4270710bfa4a6e735505753cb4a205c25fe0ff8dbd2b863387165158b4 languageName: node linkType: hard @@ -7713,7 +7713,7 @@ __metadata: "@nestjs/swagger": ^7.1.10 "@nestjs/testing": ^10.2.3 "@nestjs/websockets": ^10.2.3 - "@tonomy/tonomy-id-sdk": ^0.15.0-development.2 + "@tonomy/tonomy-id-sdk": 0.15.0-development.3 "@types/express": ^4.17.13 "@types/jest": 29.2.4 "@types/node": 18.11.18