Skip to content

Commit abd27ee

Browse files
author
James Fuqian
authored
Merge pull request CMSgov#33 from CMSgov/jfuqian/BB2-1701-Node-Sample-App-code-cleanup
[BB2-1701] Node Sample App code cleanup and re-factor to use SDK
2 parents fc07227 + 3149ba6 commit abd27ee

35 files changed

+232
-1074
lines changed

.github/workflows/ci.yaml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,3 @@ jobs:
2121

2222
- name: Lint client source
2323
run: yarn --cwd client lint
24-
25-
- name: Copy config
26-
run: cp server/src/configs/sample.config.ts server/src/configs/config.ts
27-
28-
- name: Copy .env
29-
run: cp server/src/pre-start/env/sandbox.sample.env server/src/pre-start/env/development.env
30-
31-
- name: Run server tests
32-
run: yarn --cwd server test

README-bb2-dev.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Blue Button 2.0 Sample Application Development Documentation
2+
3+
## Introduction
4+
5+
This README contains information related to developing the SDK.
6+
7+
It is intended for BB2 team members or others performing sample application development work.
8+
9+
## Run selenium tests in docker
10+
11+
Configure the remote target BB2 instance where the tested app is registered (as described above "Running the Back-end & Front-end")
12+
13+
Change your `callbackUrl` configuration to use `server` instead of `localhost`. For example:
14+
```JSON
15+
"callback_url": "http://server:3001/api/bluebutton/callback/"
16+
```
17+
18+
You can also start your configuration by the following the sample config template for selenium tests:
19+
20+
cp server/sample-bluebutton-selenium-config.json server/.bluebutton-config.json
21+
22+
You will also need to add this URL to your `redirect_uris` list in your application's configuration on the BB2 Sandbox UI.
23+
24+
Go to local repository base directory and run docker compose as below:
25+
26+
docker-compose -f docker-compose.selenium.yml up --abort-on-container-exit
27+
28+
Note: --abort-on-container-exit will abort client and server containers when selenium tests ends
29+
30+
Note: You may need to clean up already existing Docker containers, if you are having issues or have changed your configuration file.
31+
32+
## Visual trouble shoot
33+
34+
Install VNC viewer and point browser to http://localhost:5900 to monitor web UI interactions

README.md

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ Download and install node. Go to https://nodejs.org/en/download/ and follow the
2424

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

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

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

32-
copy server/src/pre-start/env/sandbox.sample.env -> server/src/pre-start/env/development.env
33-
3432
docker-compose up -d
3533

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

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

80-
## Run tests
81-
82-
Go to local repo base directory:
83-
84-
copy server/src/configs/sample.config.ts -> server/src/configs/config.ts
85-
86-
yarn --cwd server install
87-
yarn --cwd server test
88-
89-
## Run selenium tests in docker
90-
91-
Configure the remote target BB2 instance where the tested app is registered (as described above "Running the Back-end & Front-end")
92-
93-
Go to local repo base directory, from there run:
94-
95-
docker-compose -f docker-compose.selenium.yml up --abort-on-container-exit
96-
97-
Note: --abort-on-container-exit will abort client and server containers when selenium tests ends
98-
99-
## Visual trouble shoot
100-
101-
Install VNC viewer and point browser to http://localhost:5900 to monitor web UI interactions
102-
10378
## Error Responses and handling:
10479

10580
[See ErrorResponses.md](./ErrorResponses.md)

selenium_tests/src/test_node_sample.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
# Generated by Selenium IDE
21
import time
32
from selenium import webdriver
43
from selenium.webdriver.common.by import By
54
from selenium.webdriver.support import expected_conditions as EC
65
from selenium.webdriver.support.wait import WebDriverWait
76

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

910
class TestNodeSampleApp():
1011
driver_ready = False
@@ -73,6 +74,13 @@ def _assert_EOB_table_records_present(self, cnt):
7374
elements = self._find_elem_xpath(xpath)
7475
assert len(elements) == cnt
7576

77+
def _assert_EOB_table_error_present(self):
78+
element = self._find_and_return(30,
79+
By.CSS_SELECTOR,
80+
CSS_SEL_FE_ERR_MSG_CONTENT)
81+
assert element is not None
82+
print(element.text)
83+
7684
def _input_user_and_passwd_and_login(self):
7785
self._find_and_sendkey(30, By.ID, "username-textbox", "BBUser10000")
7886
self._find_and_sendkey(30, By.ID, "password-textbox", "PW10000!")
@@ -85,6 +93,7 @@ def test_node_sample_app_grant_access(self):
8593
assert elem is not None
8694
self._input_user_and_passwd_and_login()
8795
self._find_and_click(30, By.ID, "approve")
96+
time.sleep(5)
8897
self._assert_EOB_table_header_present()
8998
self._assert_EOB_table_records_present(10)
9099

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

@@ -107,5 +117,27 @@ def test_node_sample_app_deny_access(self):
107117
assert elem is not None
108118
self._input_user_and_passwd_and_login()
109119
self._find_and_click(30, By.ID, "deny")
110-
self._assert_EOB_table_header_present()
111-
self._assert_EOB_table_records_present(0)
120+
time.sleep(5)
121+
self._assert_EOB_table_error_present()
122+
123+
def test_node_sample_app_grant_followed_by_deny_access(self):
124+
'''
125+
this is to verify that the cached result from previous
126+
authorized eob query is clean up (should not see cached claims)
127+
'''
128+
self.driver.get("http://client:3000/")
129+
self.driver.set_window_size(1500, 1800)
130+
elem = self._find_and_click(30, By.ID, "auth_btn")
131+
assert elem is not None
132+
self._input_user_and_passwd_and_login()
133+
self._find_and_click(30, By.ID, "approve")
134+
time.sleep(5)
135+
self._assert_EOB_table_records_present(10)
136+
# go again
137+
elem = self._find_and_click(30, By.ID, "auth_btn")
138+
assert elem is not None
139+
self._input_user_and_passwd_and_login()
140+
self._find_and_click(30, By.ID, "deny")
141+
time.sleep(5)
142+
# should see error message
143+
self._assert_EOB_table_error_present()

server/Dockerfile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@ LABEL description="Demo of a Medicare claims data sample app"
55

66
WORKDIR /server
77

8-
COPY ["./src/configs/sample.config.ts", "./src/configs/config.ts"]
9-
COPY ["./src/pre-start/env/sandbox.sample.env","./src/pre-start/env/development.env"]
10-
118
COPY . .
129

1310
RUN yarn install

server/build.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.
76.8 KB
Binary file not shown.

server/index.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import express, { Request, Response } from "express";
2+
import { AuthorizationToken, BlueButton } from "cms-bluebutton-sdk";
3+
4+
interface User {
5+
authToken?: AuthorizationToken,
6+
eobData?: any,
7+
errors?: string[]
8+
}
9+
10+
const BENE_DENIED_ACCESS = "access_denied"
11+
const FE_MSG_ACCESS_DENIED = "Beneficiary denied app access to their data"
12+
const ERR_QUERY_EOB = "Error when querying the patient's EOB!"
13+
const ERR_MISSING_AUTH_CODE = "Response was missing access code!"
14+
const ERR_MISSING_STATE = "State is required when using PKCE"
15+
16+
const app = express();
17+
18+
const bb = new BlueButton();
19+
const authData = bb.generateAuthData();
20+
21+
// This is where medicare.gov beneficiary associated
22+
// with the current logged in app user,
23+
// in real app, this could be the app specific
24+
// account management system
25+
26+
const loggedInUser: User = {
27+
};
28+
29+
// helper to clean up cached eob data
30+
function clearBB2Data() {
31+
loggedInUser.authToken = undefined;
32+
loggedInUser.eobData = {};
33+
}
34+
35+
// AuthorizationToken holds access grant info:
36+
// access token, expire in, expire at, token type, scope, refreh token, etc.
37+
// it is associated with current logged in user in real app,
38+
// check SDK js docs for more details.
39+
40+
let authToken: AuthorizationToken;
41+
42+
// auth flow: response with URL to redirect to Medicare.gov beneficiary login
43+
app.get("/api/authorize/authurl", (req: Request, res: Response) => {
44+
res.send(bb.generateAuthorizeUrl(authData));
45+
});
46+
47+
// auth flow: oauth2 call back
48+
app.get("/api/bluebutton/callback", (req: Request, res: Response) => {
49+
(async (req: Request, res: Response) => {
50+
if (typeof req.query.error === "string") {
51+
// clear all cached claims eob data since the bene has denied access
52+
// for the application
53+
clearBB2Data();
54+
let errMsg = req.query.error;
55+
if (req.query.error === BENE_DENIED_ACCESS) {
56+
errMsg = FE_MSG_ACCESS_DENIED;
57+
}
58+
loggedInUser.eobData = {"message": errMsg};
59+
process.stdout.write(errMsg + '\n');
60+
} else {
61+
if (
62+
typeof req.query.code === "string" &&
63+
typeof req.query.state === "string"
64+
) {
65+
try {
66+
authToken = await bb.getAuthorizationToken(
67+
authData,
68+
req.query.code,
69+
req.query.state
70+
);
71+
// data flow: after access granted
72+
// the app logic can fetch the beneficiary's data in app specific ways:
73+
// e.g. download EOB periodically etc.
74+
// access token can expire, SDK automatically refresh access token when that happens.
75+
const eobResults = await bb.getExplanationOfBenefitData(authToken);
76+
authToken = eobResults.token; // in case authToken got refreshed during fhir call
77+
78+
loggedInUser.authToken = authToken;
79+
80+
loggedInUser.eobData = eobResults.response?.data;
81+
} catch (e) {
82+
loggedInUser.eobData = {};
83+
process.stdout.write(ERR_QUERY_EOB + '\n');
84+
process.stdout.write("Exception: " + e + '\n');
85+
}
86+
} else {
87+
clearBB2Data();
88+
process.stdout.write(ERR_MISSING_AUTH_CODE + '\n');
89+
process.stdout.write("OR" + '\n');
90+
process.stdout.write(ERR_MISSING_STATE + '\n');
91+
process.stdout.write("AUTH CODE: " + req.query.code + '\n');
92+
process.stdout.write("STATE: " + req.query.state + '\n');
93+
}
94+
}
95+
const fe_redirect_url =
96+
process.env.SELENIUM_TESTS ? 'http://client:3000' : 'http://localhost:3000';
97+
res.redirect(fe_redirect_url);
98+
}
99+
)(req, res);
100+
});
101+
102+
// data flow: front end fetch eob
103+
app.get("/api/data/benefit", (req: Request, res: Response) => {
104+
if (loggedInUser.eobData) {
105+
res.json(loggedInUser.eobData);
106+
}
107+
});
108+
109+
const port = 3001;
110+
app.listen(port, () => {
111+
process.stdout.write(`[server]: Server is running at https://localhost:${port}`);
112+
process.stdout.write("\n");
113+
});

0 commit comments

Comments
 (0)