Skip to content

Commit

Permalink
Merge pull request #33 from CMSgov/jfuqian/BB2-1701-Node-Sample-App-c…
Browse files Browse the repository at this point in the history
…ode-cleanup

[BB2-1701] Node Sample App code cleanup and re-factor to use SDK
  • Loading branch information
James Fuqian authored Dec 5, 2022
2 parents fc07227 + 3149ba6 commit abd27ee
Show file tree
Hide file tree
Showing 35 changed files with 232 additions and 1,074 deletions.
9 changes: 0 additions & 9 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,3 @@ jobs:

- name: Lint client source
run: yarn --cwd client lint

- name: Copy config
run: cp server/src/configs/sample.config.ts server/src/configs/config.ts

- name: Copy .env
run: cp server/src/pre-start/env/sandbox.sample.env server/src/pre-start/env/development.env

- name: Run server tests
run: yarn --cwd server test
34 changes: 34 additions & 0 deletions README-bb2-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Blue Button 2.0 Sample Application Development Documentation

## Introduction

This README contains information related to developing the SDK.

It is intended for BB2 team members or others performing sample application development work.

## Run selenium tests in docker

Configure the remote target BB2 instance where the tested app is registered (as described above "Running the Back-end & Front-end")

Change your `callbackUrl` configuration to use `server` instead of `localhost`. For example:
```JSON
"callback_url": "http://server:3001/api/bluebutton/callback/"
```

You can also start your configuration by the following the sample config template for selenium tests:

cp server/sample-bluebutton-selenium-config.json server/.bluebutton-config.json

You will also need to add this URL to your `redirect_uris` list in your application's configuration on the BB2 Sandbox UI.

Go to local repository base directory and run docker compose as below:

docker-compose -f docker-compose.selenium.yml up --abort-on-container-exit

Note: --abort-on-container-exit will abort client and server containers when selenium tests ends

Note: You may need to clean up already existing Docker containers, if you are having issues or have changed your configuration file.

## Visual trouble shoot

Install VNC viewer and point browser to http://localhost:5900 to monitor web UI interactions
29 changes: 2 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ Download and install node. Go to https://nodejs.org/en/download/ and follow the

Once you have Docker and Node installed and setup then do the following:

copy server/src/configs/sample.config.ts -> server/src/configs/config.ts
cp server/sample-bluebutton-config.json server/.bluebutton-config.json

Make sure to replace the clientId and clientSecret variables within the config file with
the ones you were provided, for your application, when you created your Blue Button Sandbox account.

copy server/src/pre-start/env/sandbox.sample.env -> server/src/pre-start/env/development.env

docker-compose up -d

This single command will create the docker container with all the necessary packages, configuration, and code to
Expand Down Expand Up @@ -66,7 +64,7 @@ To start the sample in native OS (e.g. Linux) with server and client components
1. go to the base directory of the repo
2. run below to start the server:
1. yarn --cwd server install
2. yarn --cwd server start:dev
2. yarn --cwd server start
3. run below to start the client:
1. yarn --cwd client install
2. yarn --cwd client start-native
Expand All @@ -77,29 +75,6 @@ Both ways of starting the sample are running the sample in foreground, logging a

For client and server started separately in their command window, type Ctrl C respectively

## Run tests

Go to local repo base directory:

copy server/src/configs/sample.config.ts -> server/src/configs/config.ts

yarn --cwd server install
yarn --cwd server test

## Run selenium tests in docker

Configure the remote target BB2 instance where the tested app is registered (as described above "Running the Back-end & Front-end")

Go to local repo base directory, from there run:

docker-compose -f docker-compose.selenium.yml up --abort-on-container-exit

Note: --abort-on-container-exit will abort client and server containers when selenium tests ends

## Visual trouble shoot

Install VNC viewer and point browser to http://localhost:5900 to monitor web UI interactions

## Error Responses and handling:

[See ErrorResponses.md](./ErrorResponses.md)
38 changes: 35 additions & 3 deletions selenium_tests/src/test_node_sample.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Generated by Selenium IDE
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait

CSS_SEL_FE_ERR_MSG_CONTENT = "tbody .ds-c-table__cell--align-left:nth-child(2)"


class TestNodeSampleApp():
driver_ready = False
Expand Down Expand Up @@ -73,6 +74,13 @@ def _assert_EOB_table_records_present(self, cnt):
elements = self._find_elem_xpath(xpath)
assert len(elements) == cnt

def _assert_EOB_table_error_present(self):
element = self._find_and_return(30,
By.CSS_SELECTOR,
CSS_SEL_FE_ERR_MSG_CONTENT)
assert element is not None
print(element.text)

def _input_user_and_passwd_and_login(self):
self._find_and_sendkey(30, By.ID, "username-textbox", "BBUser10000")
self._find_and_sendkey(30, By.ID, "password-textbox", "PW10000!")
Expand All @@ -85,6 +93,7 @@ def test_node_sample_app_grant_access(self):
assert elem is not None
self._input_user_and_passwd_and_login()
self._find_and_click(30, By.ID, "approve")
time.sleep(5)
self._assert_EOB_table_header_present()
self._assert_EOB_table_records_present(10)

Expand All @@ -97,6 +106,7 @@ def test_node_sample_app_grant_access_no_demographic(self):
# select radio button "No Demographic Data"
self._find_and_click(30, By.CSS_SELECTOR, "label:nth-child(5)")
self._find_and_click(30, By.ID, "approve")
time.sleep(5)
self._assert_EOB_table_header_present()
self._assert_EOB_table_records_present(10)

Expand All @@ -107,5 +117,27 @@ def test_node_sample_app_deny_access(self):
assert elem is not None
self._input_user_and_passwd_and_login()
self._find_and_click(30, By.ID, "deny")
self._assert_EOB_table_header_present()
self._assert_EOB_table_records_present(0)
time.sleep(5)
self._assert_EOB_table_error_present()

def test_node_sample_app_grant_followed_by_deny_access(self):
'''
this is to verify that the cached result from previous
authorized eob query is clean up (should not see cached claims)
'''
self.driver.get("http://client:3000/")
self.driver.set_window_size(1500, 1800)
elem = self._find_and_click(30, By.ID, "auth_btn")
assert elem is not None
self._input_user_and_passwd_and_login()
self._find_and_click(30, By.ID, "approve")
time.sleep(5)
self._assert_EOB_table_records_present(10)
# go again
elem = self._find_and_click(30, By.ID, "auth_btn")
assert elem is not None
self._input_user_and_passwd_and_login()
self._find_and_click(30, By.ID, "deny")
time.sleep(5)
# should see error message
self._assert_EOB_table_error_present()
3 changes: 0 additions & 3 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ LABEL description="Demo of a Medicare claims data sample app"

WORKDIR /server

COPY ["./src/configs/sample.config.ts", "./src/configs/config.ts"]
COPY ["./src/pre-start/env/sandbox.sample.env","./src/pre-start/env/development.env"]

COPY . .

RUN yarn install
Expand Down
63 changes: 0 additions & 63 deletions server/build.ts

This file was deleted.

Binary file added server/cms-bluebutton-sdk-1.0.0.tgz
Binary file not shown.
113 changes: 113 additions & 0 deletions server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import express, { Request, Response } from "express";
import { AuthorizationToken, BlueButton } from "cms-bluebutton-sdk";

interface User {
authToken?: AuthorizationToken,
eobData?: any,
errors?: string[]
}

const BENE_DENIED_ACCESS = "access_denied"
const FE_MSG_ACCESS_DENIED = "Beneficiary denied app access to their data"
const ERR_QUERY_EOB = "Error when querying the patient's EOB!"
const ERR_MISSING_AUTH_CODE = "Response was missing access code!"
const ERR_MISSING_STATE = "State is required when using PKCE"

const app = express();

const bb = new BlueButton();
const authData = bb.generateAuthData();

// This is where medicare.gov beneficiary associated
// with the current logged in app user,
// in real app, this could be the app specific
// account management system

const loggedInUser: User = {
};

// helper to clean up cached eob data
function clearBB2Data() {
loggedInUser.authToken = undefined;
loggedInUser.eobData = {};
}

// AuthorizationToken holds access grant info:
// access token, expire in, expire at, token type, scope, refreh token, etc.
// it is associated with current logged in user in real app,
// check SDK js docs for more details.

let authToken: AuthorizationToken;

// auth flow: response with URL to redirect to Medicare.gov beneficiary login
app.get("/api/authorize/authurl", (req: Request, res: Response) => {
res.send(bb.generateAuthorizeUrl(authData));
});

// auth flow: oauth2 call back
app.get("/api/bluebutton/callback", (req: Request, res: Response) => {
(async (req: Request, res: Response) => {
if (typeof req.query.error === "string") {
// clear all cached claims eob data since the bene has denied access
// for the application
clearBB2Data();
let errMsg = req.query.error;
if (req.query.error === BENE_DENIED_ACCESS) {
errMsg = FE_MSG_ACCESS_DENIED;
}
loggedInUser.eobData = {"message": errMsg};
process.stdout.write(errMsg + '\n');
} else {
if (
typeof req.query.code === "string" &&
typeof req.query.state === "string"
) {
try {
authToken = await bb.getAuthorizationToken(
authData,
req.query.code,
req.query.state
);
// data flow: after access granted
// the app logic can fetch the beneficiary's data in app specific ways:
// e.g. download EOB periodically etc.
// access token can expire, SDK automatically refresh access token when that happens.
const eobResults = await bb.getExplanationOfBenefitData(authToken);
authToken = eobResults.token; // in case authToken got refreshed during fhir call

loggedInUser.authToken = authToken;

loggedInUser.eobData = eobResults.response?.data;
} catch (e) {
loggedInUser.eobData = {};
process.stdout.write(ERR_QUERY_EOB + '\n');
process.stdout.write("Exception: " + e + '\n');
}
} else {
clearBB2Data();
process.stdout.write(ERR_MISSING_AUTH_CODE + '\n');
process.stdout.write("OR" + '\n');
process.stdout.write(ERR_MISSING_STATE + '\n');
process.stdout.write("AUTH CODE: " + req.query.code + '\n');
process.stdout.write("STATE: " + req.query.state + '\n');
}
}
const fe_redirect_url =
process.env.SELENIUM_TESTS ? 'http://client:3000' : 'http://localhost:3000';
res.redirect(fe_redirect_url);
}
)(req, res);
});

// data flow: front end fetch eob
app.get("/api/data/benefit", (req: Request, res: Response) => {
if (loggedInUser.eobData) {
res.json(loggedInUser.eobData);
}
});

const port = 3001;
app.listen(port, () => {
process.stdout.write(`[server]: Server is running at https://localhost:${port}`);
process.stdout.write("\n");
});
Loading

0 comments on commit abd27ee

Please sign in to comment.