diff --git a/.gitignore b/.gitignore index 9e5c966ca..29483a48e 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ wheels/ .installed.cfg *.egg +# Inferno kits +smart-app-launch-test-kit/ + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. diff --git a/Jenkinsfiles/Jenkinsfile.cbc-run-multi-pr-checks-w-selenium-chromium b/Jenkinsfiles/Jenkinsfile.cbc-run-multi-pr-checks-w-selenium-chromium index 525928651..fb841530a 100644 --- a/Jenkinsfiles/Jenkinsfile.cbc-run-multi-pr-checks-w-selenium-chromium +++ b/Jenkinsfiles/Jenkinsfile.cbc-run-multi-pr-checks-w-selenium-chromium @@ -41,6 +41,11 @@ pipeline { defaultValue: false, description: 'Set to true, selenium tests will be run as part of integration tests' ) + booleanParam( + name: 'RUN_INFERNO_TESTS', + defaultValue: false, + description: 'Set to true, inferno tests will be run as part of integration tests' + ) } stages { @@ -68,6 +73,37 @@ pipeline { } } + // Comment out to prevent hanging instance + // stage("START Inferno in background") { + // when { + // expression { params.RUN_INFERNO_TESTS == true } + // } + + // agent { + // dockerfile { + // filename 'Dockerfile' + // dir 'smart-app-launch-test-kit' + // } + // } + // steps{ + // script { + // echo 'clone inferno test kits ...' + // git clone https://github.com/inferno-framework/smart-app-launch-test-kit.git + // echo 'after clone inferno test kits ...' + // } + // dir("./smart-app-launch-test-kit/") { + // sh """ + // echo 'inferno setup, and start ...' + // docker compose pull + // docker compose build + // docker compose run inferno bundle exec inferno migrate + // docker compose up -d + // echo 'inferno start in background ...' + // """ + // } + // } + // } + stage("START BB2 server in background") { when { expression { params.RUN_SELENIUM_TESTS == true } @@ -120,6 +156,20 @@ pipeline { } } + // Comment out to prevent hanging instance + // stage("RUN inferno tests") { + // when { + // expression { params.RUN_INFERNO_TESTS == true } + // } + // steps{ + // catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { + // sh """ + // USE_NEW_PERM_SCREEN=true ON_REMOTE_CI=true pytest -s ./apps/integration_tests/selenium_inferno_tests.py + // """ + // } + // } + // } + stage("RUN integration tests") { steps{ sh """ diff --git a/README.Inferno.md b/README.Inferno.md new file mode 100644 index 000000000..972516786 --- /dev/null +++ b/README.Inferno.md @@ -0,0 +1,35 @@ +Run Inferno Test Kits +===================================================== +BB2 integration test suite include Inferno test kit(s) +(in this POC, picked Smart App Launch Test Kit for demo purpose, pull in as many inferno kits as needed by compliance requirement) + +Setup +----- + +At the base directory of the local repository, run below command to download, install, and start the inferno service + +```bash +./docker-compose/start_inferno.sh +``` + +Note: the process will run in current terminal and can be terminated by CTRL+C + +Run tests +---------- + +Run inferno tests (selenium based) against local BB2 (assuming it is already started), as below example: + +```bash +./docker-compose/run_inferno_tests.sh -p LOCAL +``` + +For help: +```bash +./docker-compose/run_inferno_tests.sh -h +``` + +Run inferno tests against remote BB2 (PROD, SBX, TEST) as below example: + +```bash +./docker-compose/run_inferno_tests.sh -p TEST +``` diff --git a/apps/integration_tests/selenium_cases.py b/apps/integration_tests/selenium_cases.py index 5892345ee..685ceed87 100755 --- a/apps/integration_tests/selenium_cases.py +++ b/apps/integration_tests/selenium_cases.py @@ -666,6 +666,15 @@ class Action(Enum): ] } +INFERNO_TESTS = { + "auth_grant_pkce_fhir_calls": [ + {"sequence": SEQ_AUTHORIZE_PKCE_START}, + CALL_LOGIN, + CLICK_AGREE_ACCESS, + {"sequence": SEQ_QUERY_FHIR_RESOURCES} + ], +} + SEQ_CREATE_USER_ACCOUNT = [ { "display": "Load BB2 Landing Page ...", diff --git a/apps/integration_tests/selenium_generic.py b/apps/integration_tests/selenium_generic.py index a7c4a57b0..4598e6177 100644 --- a/apps/integration_tests/selenium_generic.py +++ b/apps/integration_tests/selenium_generic.py @@ -50,6 +50,7 @@ def setup_method(self, method): self.selenium_grid_host = os.getenv('SELENIUM_GRID_HOST', "chrome") self.selenium_grid = os.getenv('SELENIUM_GRID', "false") self.hostname_url = os.environ['HOSTNAME_URL'] + print("HOSTNAME_URL={}".format(self.hostname_url)) self.use_mslsx = os.environ['USE_MSLSX'] self.login_seq = SEQ_LOGIN_MSLSX if self.use_mslsx == 'true' else SEQ_LOGIN_SLSX msg_fmt = "use_mslsx={}, hostname_url={}, selenium_grid={}" diff --git a/apps/integration_tests/selenium_inferno_tests.py b/apps/integration_tests/selenium_inferno_tests.py new file mode 100644 index 000000000..eba7b8852 --- /dev/null +++ b/apps/integration_tests/selenium_inferno_tests.py @@ -0,0 +1,108 @@ +import time +import os + +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait +from selenium.webdriver.common.by import By +from .selenium_generic import SeleniumGenericTests +from .selenium_cases import ( + SLSX_TXT_FLD_PASSWORD_VAL, + SLSX_TXT_FLD_USERNAME_VAL +) + +USE_NEW_PERM_SCREEN = "true" + + +class TestInfernoSuites(SeleniumGenericTests): + def test_inferno_suites(self): + self.inferno_url = os.getenv('INFERNO_URL', 'http://localhost') + self.test_app_client_id = os.getenv('CLIENT_ID_4_INFERNO_TEST', 'client_id_of_built_in_testapp') + # recently created SSM entry return the result in the form of {secret_id:secretvalue} + # extract the value if so + self.test_app_client_id = self._extract_value(self.test_app_client_id) + self.test_app_client_secret = os.getenv('CLIENT_SECRET_4_INFERNO_TEST', 'client_secret_of_built_in_testapp') + self.test_app_client_secret = self._extract_value(self.test_app_client_secret) + + driver = self.driver + driver.get(self.inferno_url) + # in this recorded raw code, sleep called before each find element call to allow dom model to populate and the widget + # become visible, if we use BB2 selenium ACTION FIND_CLICK (call WebDriverWait under hood) + # e.g. it will be with 30 sec timeout + # TODO: convert into BB2 selenium test ACTIONs... + time.sleep(2) + driver.find_element(By.XPATH, "//*[text()='SMART App Launch STU2.2']").click() + time.sleep(2) + driver.find_element(By.XPATH, "//div[@id='root']/div/div[2]/div/div/div[2]/div/div[3]/div/span").click() + time.sleep(2) + driver.find_element(By.XPATH, "//button[@type='button']").click() + time.sleep(2) + driver.find_element(By.XPATH, "//div[@id='root']/div/div/main/div/div/div/span[2]/button").click() + time.sleep(2) + driver.find_element(By.ID, "input0_text").click() + time.sleep(2) + driver.find_element(By.ID, "input0_text").clear() + time.sleep(2) + driver.find_element(By.ID, "input0_text").send_keys("{}/v2/fhir/".format(self.hostname_url)) + time.sleep(2) + driver.find_element(By.ID, "input1_autocomplete").click() + time.sleep(2) + driver.find_element(By.ID, "input1_autocomplete-option-1").click() + time.sleep(2) + # clear preset scope and put in bb2 scope + driver.find_element(By.ID, "input3_text").click() + time.sleep(5) + driver.find_element(By.ID, "input3_text").clear() + time.sleep(5) + scope = "launch/patient openid patient/Patient.rs patient/Coverage.rs patient/ExplanationOfBenefit.rs" + driver.find_element(By.ID, "input3_text").send_keys(scope) + time.sleep(5) + # click client id field and put in value + driver.find_element(By.ID, "input4_text").click() + time.sleep(5) + driver.find_element(By.ID, "input4_text").send_keys(self.test_app_client_id) + time.sleep(5) + # click client secret field and put in value + driver.find_element(By.ID, "input5_text").click() + time.sleep(5) + driver.find_element(By.ID, "input5_text").send_keys(self.test_app_client_secret) + time.sleep(5) + # click client id field and put in value AGAIN + driver.find_element(By.ID, "input4_text").click() + time.sleep(5) + # challenge: SUBMIT button refuse to become active in selenium play which prevent + # test proceed + elem = WebDriverWait( + self.driver, 20).until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".MuiButton-containedSizeMedium"))) + if elem: + # attempt force click by exec javascript (not working yet) + driver.execute_script("arguments[0].click();", elem) + else: + print("Submit not visible in 30 sec...") + # if submit got clicked, there is period before a user action dialog popup + time.sleep(50) + # below is the inferno user action dialog + driver.find_element(By.LINK_TEXT, "Follow this link to authorize with the SMART server").click() + time.sleep(5) + driver.find_element(By.ID, "username-textbox").click() + time.sleep(5) + driver.find_element(By.ID, "username-textbox").clear() + time.sleep(5) + driver.find_element(By.ID, "username-textbox").send_keys(SLSX_TXT_FLD_USERNAME_VAL) + time.sleep(5) + driver.find_element(By.ID, "password-textbox").click() + time.sleep(5) + driver.find_element(By.ID, "password-textbox").clear() + time.sleep(5) + driver.find_element(By.ID, "password-textbox").send_keys(SLSX_TXT_FLD_PASSWORD_VAL) + time.sleep(5) + driver.find_element(By.ID, "login-button").click() + time.sleep(5) + time.sleep(5) + driver.find_element(By.ID, "approve").click() + time.sleep(5) + + def _extract_value(self, s): + if s is not None and s.startswith("{"): + ll = s.strip("{}").split(":") + return ll[1] + return s diff --git a/apps/testclient/management/commands/create_test_user_and_application.py b/apps/testclient/management/commands/create_test_user_and_application.py index 5febcf40a..e592c6abf 100644 --- a/apps/testclient/management/commands/create_test_user_and_application.py +++ b/apps/testclient/management/commands/create_test_user_and_application.py @@ -90,18 +90,36 @@ def create_application(user, group, app, redirect): redirect_uri = "https://" + redirect_uri a = Application.objects.create(name=app_name, - redirect_uris=redirect_uri, + redirect_uris=redirect_uri + " " + "http://localhost/custom/smart_stu2_2/redirect", user=user, data_access_type="THIRTEEN_MONTH", client_type="confidential", + client_id="client_id_of_built_in_testapp", + client_secret="client_secret_of_built_in_testapp", + client_secret_plain="client_secret_of_built_in_testapp", authorization_grant_type="authorization-code") - titles = ["My Medicare and supplemental coverage information.", - "My Medicare claim information.", - "My general patient and demographic information.", - "Profile information including name and email." + titles = [ + "My general patient and demographic information.", + "Profile information including name and email.", + "My Medicare claim information.", + "My Medicare and supplemental coverage information.", + "Token Management", + "Token Introspect", + "Openid profile permissions.", + "Read my general patient and demographic information.", + "Search my general patient and demographic information.", + "Read and search my general patient and demographic information.", + "Read my Medicare claim information.", + "Search my Medicare claim information.", + "Read and search my Medicare claim information.", + "Read my Medicare and supplemental coverage information.", + "Search my Medicare and supplemental coverage information.", + "Read and search my Medicare and supplemental coverage information.", + "Patient launch context." ] + for t in titles: c = ProtectedCapability.objects.get(title=t) a.scope.add(c) diff --git a/docker-compose.inferno.yml b/docker-compose.inferno.yml new file mode 100755 index 000000000..a740b7413 --- /dev/null +++ b/docker-compose.inferno.yml @@ -0,0 +1,23 @@ +services: + inferno-tests: + build: + context: ./ + dockerfile: Dockerfile.selenium + command: pytest ./apps/integration_tests/selenium_inferno_tests.py + ports: + - "8910:8910" + env_file: + - docker-compose/selenium-env-vars.env + - docker-compose/inferno-env-vars.env + volumes: + - .:/code + depends_on: + chrome: + condition: service_started + + chrome: + image: selenium/standalone-chromium + hostname: chrome + ports: + - "4444:4444" + - "5900:5900" diff --git a/docker-compose/inferno-env-vars.env b/docker-compose/inferno-env-vars.env new file mode 100644 index 000000000..d35702b86 --- /dev/null +++ b/docker-compose/inferno-env-vars.env @@ -0,0 +1,3 @@ +CLIENT_ID_4_INFERNO_TEST=${DJANGO_CLIENT_ID_4_INFERNO_TEST} +CLIENT_SECRET_4_INFERNO_TEST=${DJANGO_CLIENT_SECRET_4_INFERNO_TEST} +INFERNO_URL=${INFERNO_URL} diff --git a/docker-compose/run_inferno_tests.sh b/docker-compose/run_inferno_tests.sh new file mode 100755 index 000000000..5846296b2 --- /dev/null +++ b/docker-compose/run_inferno_tests.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# Run the inferno tests (selenium based) against BB2 server (LOCAL|PROD/SBX/TEST) +# +# NOTE: +# +# 1. You must be logged in to AWS CLI. +# +# 2. You must also be connected to the VPN. +# +# SETTINGS: You may need to customize these for your local setup. + +# Echo function that includes script name on each line for console log readability +echo_msg () { + echo "$(basename $0): $*" +} + +display_usage() { + echo + echo "Usage:" + echo "------------------" + echo + echo "Syntax: run_inferno_tests.sh [-h|p|g|t] [LOCAL|SBX|PROD|TEST|]" + echo + echo "Options:" + echo + echo "-h Print this Help." + echo "-p Test for newer permissions screen during oauth user login. Defaults to older screen." + echo "-g Selenium grid used." + echo "-t Show test case actions on std out." + echo + echo "Examples:" + echo + echo "run_inferno_tests.sh -p https://localhost:8000/ (or LOCAL)" + echo + echo "run_inferno_tests.sh -p https://sandbox.bluebutton.cms.gov/ (or SBX)" + echo + echo "run_inferno_tests.sh https://api.bluebutton.cms.gov/ (or PROD)" + echo + echo "run_inferno_tests.sh -p https://test.bluebutton.cms.gov/ (or TEST)" + echo + echo " default to SBX (https://sandbox.bluebutton.cms.gov/)" + echo + echo +} + +# main +echo_msg +echo_msg RUNNING SCRIPT: ${0} +echo_msg + +# Set bash builtins for safety +set -e -u -o pipefail + +# export INFERNO_URL="http://localhost" +export INFERNO_URL="http://192.168.0.146" +export USE_NEW_PERM_SCREEN=false +export SERVICE_NAME="inferno-tests" +export TESTS_LIST="./apps/integration_tests/selenium_inferno_tests.py" +# BB2 service end point default (SBX) +export HOSTNAME_URL="https://sandbox.bluebutton.cms.gov/" +# selenium grid +export SELENIUM_GRID=false +# Show test actions on std out : pytest -s +PYTEST_SHOW_TRACE_OPT='' + +while getopts "hpgt" option; do + case $option in + h) + display_usage; + exit;; + p) + export USE_NEW_PERM_SCREEN=true;; + g) + export SELENIUM_GRID=true;; + t) + export PYTEST_SHOW_TRACE_OPT='-s';; + \?) + display_usage; + exit;; + esac +done + +eval last_arg=\$$# + +echo "last arg: " $last_arg + +if [[ -n ${last_arg} ]] +then + case "${last_arg}" in + LOCAL) + export HOSTNAME_URL="http://localhost:8000/" + ;; + SBX) + export HOSTNAME_URL="https://sandbox.bluebutton.cms.gov/" + ;; + PROD) + export HOSTNAME_URL="https://api.bluebutton.cms.gov/" + ;; + TEST) + export HOSTNAME_URL="https://test.bluebutton.cms.gov/" + ;; + *) + if [[ ${last_arg} == 'http'* ]] + then + export HOSTNAME_URL=${last_arg} + else + echo "Invalid argument: " ${last_arg} + display_usage + exit 1 + fi + ;; + esac +fi + +# Set SYSTEM +SYSTEM=$(uname -s) + +echo "USE_NEW_PERM_SCREEN=" ${USE_NEW_PERM_SCREEN} +echo "BB2 Server URL=" ${HOSTNAME_URL} +echo "Selenium grid=" ${SELENIUM_GRID} + +export USE_NEW_PERM_SCREEN +export USE_MSLSX=false + +if [[ -n ${last_arg} ]] +then + case "${last_arg}" in + LOCAL) + export HOSTNAME_URL="http://localhost:8000/" + export DJANGO_CLIENT_ID_4_INFERNO_TEST="client_id_of_built_in_testapp" + export DJANGO_CLIENT_SECRET_4_INFERNO_TEST="client_secret_of_built_in_testapp" + ;; + *) + # Inferno test app creds + export DJANGO_CLIENT_ID_4_INFERNO_TEST=$(aws secretsmanager get-secret-value --secret-id /bb2/test/app/inferno_test_client_id --query 'SecretString' --output text) + export DJANGO_CLIENT_SECRET_4_INFERNO_TEST=$(aws secretsmanager get-secret-value --secret-id /bb2/test/app/inferno_test_client_secret --query 'SecretString' --output text) + ;; + esac +fi + +# assume the target bb2 server is up, either local or remote +docker compose -f docker-compose.inferno.yml run inferno-tests bash -c "HOSTNAME_URL=${HOSTNAME_URL} CLIENT_ID_4_INFERNO_TEST=${DJANGO_CLIENT_ID_4_INFERNO_TEST} CLIENT_SECRET_4_INFERNO_TEST=${DJANGO_CLIENT_SECRET_4_INFERNO_TEST} INFERNO_URL=${INFERNO_URL} SELENIUM_GRID=${SELENIUM_GRID} pytest ${PYTEST_SHOW_TRACE_OPT} ${TESTS_LIST}" + +# Stop containers after use +echo_msg +echo_msg "Stopping containers..." +echo_msg + +docker compose -f docker-compose.inferno.yml stop diff --git a/docker-compose/start_inferno.sh b/docker-compose/start_inferno.sh new file mode 100755 index 000000000..73fa88451 --- /dev/null +++ b/docker-compose/start_inferno.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# start inferno by: +# using smart-app-launch-test-kit as example. +# git clone , e.g. https://github.com/inferno-framework/smart-app-launch-test-kit.git +# cd smart-app-launch-test-kit +# ./setup.sh +# ./run.sh + +if [ ! -d 'smart-app-launch-test-kit' ] +then + git clone https://github.com/inferno-framework/smart-app-launch-test-kit.git +else + echo 'smart-app-launch-test-kit already checked out.' +fi + +echo "Start Inferno: smart-app-launch-test-kit ..." +cd smart-app-launch-test-kit ; ./setup.sh ; ./run.sh +echo "After start Inferno: smart-app-launch-test-kit ..."