diff --git a/integration-tests/SubscriptionsHelper.ts b/integration-tests/SubscriptionsHelper.ts new file mode 100644 index 00000000..9672cfd2 --- /dev/null +++ b/integration-tests/SubscriptionsHelper.ts @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + * + */ +import * as AWS from 'aws-sdk'; +import { DynamoDB } from 'aws-sdk'; + +export interface SubscriptionNotification { + httpMethod: string; + path: string; + body?: string | null; + headers?: string[]; +} + +// eslint-disable-next-line import/prefer-default-export +export class SubscriptionsHelper { + private readonly notificationsTableName: string; + + private readonly dynamodbClient: DynamoDB; + + constructor(notificationsTableName: string) { + this.notificationsTableName = notificationsTableName; + this.dynamodbClient = new AWS.DynamoDB(); + } + + /** + * Gets all notifications received for a given path. + * @param path - The path where the notifications were sent. It is recommended to use unique paths for each test run (e.g. by appending an uui to it) + */ + async getNotifications(path: string): Promise { + const { Items } = await this.dynamodbClient + .query({ + TableName: this.notificationsTableName, + KeyConditionExpression: '#path = :pathValue', + ExpressionAttributeNames: { '#path': 'path' }, + ExpressionAttributeValues: { ':pathValue': { S: path } }, + }) + .promise(); + + if (Items === undefined) { + return []; + } + + return Items.map((item) => AWS.DynamoDB.Converter.unmarshall(item) as SubscriptionNotification); + } +} diff --git a/integration-tests/infrastructure/subscriptions-endpoint/.gitignore b/integration-tests/infrastructure/subscriptions-endpoint/.gitignore new file mode 100644 index 00000000..5e5fb2b4 --- /dev/null +++ b/integration-tests/infrastructure/subscriptions-endpoint/.gitignore @@ -0,0 +1,209 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/osx,node,linux,windows,sam +# Edit at https://www.toptal.com/developers/gitignore?templates=osx,node,linux,windows,sam + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env*.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Storybook build outputs +.out +.storybook-out +storybook-static + +# rollup.js default build output +dist/ + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ + +### OSX ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### SAM ### +# Ignore build directories for the AWS Serverless Application Model (SAM) +# Info: https://aws.amazon.com/serverless/sam/ +# Docs: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-reference.html + +**/.aws-sam +samconfig.toml + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/osx,node,linux,windows,sam diff --git a/integration-tests/infrastructure/subscriptions-endpoint/README.md b/integration-tests/infrastructure/subscriptions-endpoint/README.md new file mode 100644 index 00000000..147466ce --- /dev/null +++ b/integration-tests/infrastructure/subscriptions-endpoint/README.md @@ -0,0 +1,16 @@ +# FHIR Subscriptions Test Endpoint + +This small SAM app creates an API (APIGW + Lambda) that can be used as an endpoint in FHIR Subscriptions integration tests. + +The Lambda function replies successfully to all requests and records the notification message in DynamoDB. +The integration tests can query DynamoDB to verify that notifications were received. + +To build and deploy your application for the first time, run the following in your shell: + +```bash +sam build +sam deploy --guided +``` + +Use the outputs of the CFN stack as values for the `SUBSCRIPTIONS_NOTIFICATIONS_TABLE` and `SUBSCRIPTIONS_ENDPOINT` environment variables when running the integration tests + diff --git a/integration-tests/infrastructure/subscriptions-endpoint/src/app.js b/integration-tests/infrastructure/subscriptions-endpoint/src/app.js new file mode 100644 index 00000000..0aa4c5c6 --- /dev/null +++ b/integration-tests/infrastructure/subscriptions-endpoint/src/app.js @@ -0,0 +1,50 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +const AWS = require('aws-sdk'); + +/** + * + * Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format + * @param {Object} event - API Gateway Lambda Proxy Input Format + * + * Context doc: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html + * @param {Object} context + * + * Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html + * @returns {Object} object - API Gateway Lambda Proxy Output Format + * + */ +const dynamoDb = new AWS.DynamoDB(); + +const { TABLE_NAME } = process.env; + +if (TABLE_NAME === undefined) { + throw new Error(`Required env variable TABLE_NAME is not defined`); +} + +exports.lambdaHandler = async (event) => { + const { path, httpMethod, headers, body } = event; + + const item = { + path, + timestamp: Date.now(), + httpMethod, + headers, + body, + }; + + console.log('Received notification:', JSON.stringify(item, null, 2)); + + await dynamoDb + .putItem({ + TableName: TABLE_NAME, + Item: AWS.DynamoDB.Converter.marshall(item), + }) + .promise(); + + return { + statusCode: 200, + body: JSON.stringify({ + message: 'ok', + }), + }; +}; diff --git a/integration-tests/infrastructure/subscriptions-endpoint/src/package.json b/integration-tests/infrastructure/subscriptions-endpoint/src/package.json new file mode 100644 index 00000000..1743d677 --- /dev/null +++ b/integration-tests/infrastructure/subscriptions-endpoint/src/package.json @@ -0,0 +1,13 @@ +{ + "name": "fhir-subscriptions-test-endpoint", + "version": "1.0.0", + "description": "Simple endpoint used for FHIR Subscriptions integ tests", + "main": "app.js", + "license": "Apache-2.0", + "dependencies": { + }, + "scripts": { + }, + "devDependencies": { + } +} diff --git a/integration-tests/infrastructure/subscriptions-endpoint/template.yaml b/integration-tests/infrastructure/subscriptions-endpoint/template.yaml new file mode 100644 index 00000000..3104c812 --- /dev/null +++ b/integration-tests/infrastructure/subscriptions-endpoint/template.yaml @@ -0,0 +1,68 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + Simple endpoint used for FHIR Subscriptions integ tests + +Globals: + Function: + Timeout: 10 + +Resources: + TestNotificationsApi: + Type: AWS::Serverless::Api + Properties: + StageName: Prod + Auth: + ApiKeyRequired: true + UsagePlan: + CreateUsagePlan: PER_API + + SubEndpointFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: app.lambdaHandler + Runtime: nodejs14.x + Architectures: + - x86_64 + Policies: + - DynamoDBWritePolicy: + TableName: !Ref NotificationsTable + Environment: + Variables: + TABLE_NAME: !Ref NotificationsTable + Events: + ApiEvent: + Type: Api + Properties: + RestApiId: !Ref TestNotificationsApi + Path: /{proxy+} + Method: any + Auth: + ApiKeyRequired: true + + NotificationsTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: path + AttributeType: S + - AttributeName: timestamp + AttributeType: N + KeySchema: + - AttributeName: path + KeyType: HASH + - AttributeName: timestamp + KeyType: RANGE + BillingMode: PAY_PER_REQUEST + +Outputs: + Endpoint: + Description: "API Gateway endpoint URL" + Value: !Sub "https://${TestNotificationsApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" + ApiKey: + Description: "You can find your API Key in the AWS console" + Value: !Sub "https://console.aws.amazon.com/apigateway/home?region=${AWS::Region}#/api-keys/${TestNotificationsApiApiKey}" + NotificationsTable: + Description: "Notifications table" + Value: !Ref NotificationsTable diff --git a/integration-tests/subscriptions.test.ts b/integration-tests/subscriptions.test.ts new file mode 100644 index 00000000..21a1fdce --- /dev/null +++ b/integration-tests/subscriptions.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + * + */ + +import * as AWS from 'aws-sdk'; +import { SubscriptionsHelper } from './SubscriptionsHelper'; + +jest.setTimeout(300_000); + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { SUBSCRIPTIONS_ENABLED, SUBSCRIPTIONS_NOTIFICATIONS_TABLE, SUBSCRIPTIONS_ENDPOINT, API_AWS_REGION } = + process.env; + +if (API_AWS_REGION === undefined) { + throw new Error('API_AWS_REGION environment variable is not defined'); +} + +AWS.config.update({ region: API_AWS_REGION }); + +test('empty test placeholder', () => { + // empty test to avoid the "Your test suite must contain at least one test." error +}); + +if (SUBSCRIPTIONS_ENABLED === 'true') { + describe('FHIR Subscriptions', () => { + let subscriptionsHelper: SubscriptionsHelper; + + beforeAll(() => { + if (SUBSCRIPTIONS_NOTIFICATIONS_TABLE === undefined) { + throw new Error('SUBSCRIPTIONS_NOTIFICATIONS_TABLE environment variable is not defined'); + } + subscriptionsHelper = new SubscriptionsHelper(SUBSCRIPTIONS_NOTIFICATIONS_TABLE); + }); + + test('test', async () => { + const x = await subscriptionsHelper.getNotifications('/lala'); + console.log(x); + }); + }); +}