-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from jupyterhealth/sandbox-testing-in-ci
Sandbox testing in CI
- Loading branch information
Showing
7 changed files
with
479 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
name: JupyterHealth SMART on FHIR test suite | ||
|
||
on: | ||
push: | ||
branches: [ main ] | ||
pull_request: | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
strategy: | ||
matrix: | ||
python-version: [3.12] # extend if needed | ||
|
||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Use Node.js | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: '18.x' | ||
- name: Set up Python ${{ matrix.python-version }} | ||
uses: actions/setup-python@v5 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
cache: "pip" | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install -e .[testing] | ||
- name: Install SMART sandbox | ||
run: | | ||
git clone https://github.com/smart-on-fhir/smart-launcher-v2.git | ||
cd smart-launcher-v2 | ||
git switch -c aa0f3b1 # Fix the version we use for the sandbox | ||
npm ci | ||
npm run build | ||
env: | ||
PORT: 5555 | ||
- name: Run tests | ||
run: | | ||
pytest tests/ | ||
env: | ||
SANDBOX_DIR: ${{ github.workspace }}/smart-launcher-v2 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import pytest | ||
import os | ||
import subprocess | ||
import requests | ||
import time | ||
from dataclasses import asdict, field, dataclass | ||
import base64 | ||
import json | ||
from urllib import parse | ||
|
||
|
||
@pytest.fixture(scope="function") # module? | ||
def sandbox(): | ||
port = 5555 | ||
os.environ["PORT"] = str(port) | ||
url = f"http://localhost:{port}" | ||
with subprocess.Popen( | ||
["npm", "run", "start:prod"], cwd=os.environ.get("SANDBOX_DIR", ".") | ||
) as sandbox_proc: | ||
wait_for_server(url) | ||
yield url | ||
sandbox_proc.terminate() | ||
|
||
|
||
def wait_for_server(url): | ||
for _ in range(10): | ||
try: | ||
response = requests.get(url) | ||
if response.status_code == 200: | ||
break | ||
except requests.ConnectionError: | ||
pass | ||
time.sleep(1) # Wait for 1 second before retrying | ||
else: | ||
raise requests.ConnectionError(f"Cannot connect to {url}") | ||
|
||
|
||
@dataclass | ||
class SandboxConfig: | ||
"""Taken from smart-on-fhir/smart-launcher-v2.git:src/isomorphic/LaunchOptions.ts. | ||
The sandbox reads its configuration from the url it is launched with. | ||
This means we don't have to introduce a webdriver to manipulate its behaviour, | ||
but instead we can reverse engineer the parameters to set the necessary parameters | ||
""" | ||
|
||
# Caveat: make sure not to change the order | ||
# the smart sandbox uses list indices to evaluate which value is which property | ||
# Assumes client identity validation = true | ||
launch_type: int = 0 # provider EHR launch | ||
patient_ids: list[str] = field(default_factory=list) | ||
provider_ids: list[str] = field(default_factory=list) | ||
encounter_type: str = "AUTO" | ||
misc_skip_login: int = 0 # not compatible with provider EHR launch | ||
misc_skip_auth: int = 0 # not compatible with provider EHR launch | ||
misc_simulate_launch: int = 0 # don't simulate launch within EHR UI | ||
allowed_scopes: set[str] = field(default_factory=set) | ||
redirect_uris: list[str] = field(default_factory=list) | ||
client_id: str = "client_id" | ||
client_secret: str = "" | ||
auth_error: int = 0 # simulate no error | ||
jwks_url: str = "" | ||
jwks: str = "" | ||
client_type: int = 0 # 0 (public), 1 (symmetric), 2 (asymmetric) | ||
pkce_validation: int = 1 # 0 (none), 1 (auto), 2 (always) | ||
fhir_base_url: str = "" # arranged server side | ||
|
||
def get_launch_code(self) -> str: | ||
"""The sandbox settings are encoded in a base64 JSON object. | ||
Enforcing settings/procedures needs to be done here | ||
""" | ||
|
||
attr_list = [] | ||
for val in asdict(self).values(): | ||
if isinstance(val, int) or isinstance(val, str): | ||
attr_list.append(val) | ||
elif isinstance(val, list): | ||
attr_list.append(", ".join(val)) | ||
elif isinstance(val, set): | ||
attr_list.append(" ".join(val)) | ||
attr_repr = json.dumps(attr_list) | ||
return base64.b64encode(attr_repr.encode("utf-8")) | ||
|
||
def get_url_query( | ||
self, launch_url: str, validation: bool = True, fhir_version: str = "r4" | ||
) -> str: | ||
"""Provide the entire URL query that loads the sandbox with the given settings. | ||
Requires appending with base url""" | ||
query = parse.urlencode( | ||
{ | ||
"launch": self.get_launch_code(), | ||
"launch_url": launch_url, | ||
"validation": int(validation), | ||
"fhir_version": fhir_version, | ||
} | ||
) | ||
return query |
Oops, something went wrong.