diff --git a/.gitignore b/.gitignore index 55d27b06..c52ab0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,8 @@ selenium-debug.log .vscode coverage .nyc_output -tests/e2e/selenium-server.log -tests/e2e/bin/chromedriver -tests/e2e/bin/selenium.jar -tests/e2e/reports/login-screen.xml -tests/e2e/reports/main.xml +test/e2e/selenium-server.log +test/e2e/bin/chromedriver +test/e2e/bin/selenium.jar +test/e2e/reports/login-screen.xml +test/e2e/reports/main.xml diff --git a/benchmark.js b/benchmark.js deleted file mode 100644 index 365b9ca9..00000000 --- a/benchmark.js +++ /dev/null @@ -1,85 +0,0 @@ -const Lib = require("./src/lib"); -require("colors"); - -const BASELINES = { - "Lib.buildUrlPath" : 0.00158, - "Lib.normalizeUrl" : 0.00009, - "Lib.adjustRequestBody" : 0.00088, - "Lib.adjustUrl" : 0.00112, - "Lib.augmentConformance" : 0.00469, - "Lib.unBundleResource" : 0.00098, - "Lib.adjustResponseUrls" : 0.00296 -}; - -// https://github.com/uxitten/polyfill/blob/master/string.polyfill.js -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd -if (!String.prototype.padEnd) { - String.prototype.padEnd = function padEnd(targetLength,padString) { - targetLength = targetLength>>0; //floor if number or convert non-number to 0; - padString = String(padString || ' '); - if (this.length > targetLength) { - return String(this); - } - else { - targetLength = targetLength-this.length; - if (targetLength > padString.length) { - padString += padString.repeat(targetLength/padString.length); //append to original to ensure we are longer than needed - } - return String(this) + padString.slice(0,targetLength); - } - }; -} - -function test(label, fn, ...args) { - let n = 100000; - let precision = 3; - let mul = Math.pow(10, precision); - let i = n; - let start = Date.now(); - while (--i) { - fn(...args); - } - - let time = Math.round(((Date.now() - start) / n) * mul) / mul; - let msg = label.split(".").pop().bold.white + " " - msg = (msg.padEnd(50, "…") + " " + time.toFixed(precision).bold + " ms").grey; - if (label in BASELINES) { - let diff = Math.round((time - BASELINES[label]) * mul) / mul; - msg += (" (" + (diff < 0 ? diff : "+" + diff) + "ms)")[ - diff > 0 ? "red" : diff < 0 ? "green" : "blue" - ] - } - console.log(msg); -} - -console.log("\n"); - -// Sanboxify ------------------------------------------------------------------- -console.log(("📂 Lib".bold + "\n".padEnd(52, "–")).gray); -test("Lib.buildUrlPath", Lib.buildUrlPath, "/abxc/", "/asdb/") -test("Lib.normalizeUrl", Lib.normalizeUrl, "dasdasasda/sda/sd/asd/asdasdas") -test("Lib.adjustRequestBody", Lib.adjustRequestBody, { resource: {} }, "system", ["sb1", "sb2"]) -test("Lib.adjustUrl", Lib.adjustUrl, "a/b/c/?f=5", true, ["sb1", "sb2"]) -test("Lib.augmentConformance", Lib.augmentConformance, {rest: {}}, "authBaseUrl") -test("Lib.unBundleResource", Lib.unBundleResource, '{"entry":[{"resource":1}]}'); -test( - "Lib.adjustResponseUrls", - Lib.adjustResponseUrls, - "This is a test", // bodyText - "a", // fhirUrl - "s", // requestUrl - "d", // fhirBaseUrl - "f" // requestBaseUrl -); - -// Lib ------------------------------------------------------------------------- -// test("Lib.getPath", Lib.getPath); -// test("Lib.generateRefreshToken", Lib.generateRefreshToken); -// test("Lib.printf", Lib.printf); -// test("Lib.redirectWithError", Lib.redirectWithError); -// test("Lib.replyWithError", Lib.replyWithError); -// test("Lib.getErrorText", Lib.getErrorText); -// test("Lib.getFirstMissingProperty", Lib.getFirstMissingProperty); -// test("Lib.htmlEncode", Lib.htmlEncode); - -console.log("\n"); diff --git a/nightwatch.conf.js b/nightwatch.conf.js index 4fb6a608..5493153e 100644 --- a/nightwatch.conf.js +++ b/nightwatch.conf.js @@ -1,20 +1,20 @@ module.exports = { - "src_folders" : ["tests/e2e/spec"], - "output_folder" : "tests/e2e/reports", + "src_folders" : ["test/e2e/spec"], + "output_folder" : "test/e2e/reports", "custom_commands_path" : "", "custom_assertions_path" : "", "page_objects_path" : "", - "globals_path" : "tests/e2e/globals.js", + "globals_path" : "test/e2e/globals.js", "selenium" : { "start_process" : true, - "server_path" : "tests/e2e/bin/selenium.jar", - "log_path" : "tests/e2e", + "server_path" : "test/e2e/bin/selenium.jar", + "log_path" : "test/e2e", "host" : "127.0.0.1", "port" : 4444, "cli_args" : { - "webdriver.chrome.driver" : "tests/e2e/bin/chromedriver", + "webdriver.chrome.driver" : "test/e2e/bin/chromedriver", "webdriver.ie.driver" : "" } }, @@ -27,7 +27,7 @@ module.exports = { "silent": true, "screenshots" : { "enabled" : false, - "path" : "tests/e2e/screenshots" + "path" : "test/e2e/screenshots" }, "desiredCapabilities": { "browserName": "chrome", diff --git a/selenium-download.js b/selenium-download.js index df3e8dfb..d020abfb 100644 --- a/selenium-download.js +++ b/selenium-download.js @@ -1,5 +1,5 @@ var selenium = require('selenium-download'); -selenium.ensure('./tests/e2e/bin', function(error) { +selenium.ensure('./test/e2e/bin', function(error) { if (error) { console.error(error.stack); } diff --git a/src/lib.js b/src/lib.js index becd1543..6f302db0 100644 --- a/src/lib.js +++ b/src/lib.js @@ -41,7 +41,7 @@ function getPath(obj, path = "") { * whatever is supplied in the rest of the arguments. If no argument is supplied * the "%s" token is left as is. * @param {String} s The string to format - * @param {*} ... The rest of the arguments are used for the replacements + * @param {*[]} ... The rest of the arguments are used for the replacements * @return {String} */ function printf(s) { @@ -78,7 +78,9 @@ function redirectWithError(req, res, name, ...rest) { } function replyWithError(res, name, code = 500, ...params) { - return res.status(code).send(getErrorText(name, ...params)); + res.status(code) + res.set('Content-Type', 'text/plain') + return res.send(getErrorText(name, ...params)); } function getErrorText(name, ...rest) { @@ -194,7 +196,7 @@ function augmentConformance(bodyText, authBaseUrl) { let json; try { json = JSON.parse(bodyText); - if (!json.rest[0].security){ + if (!json.rest[0].security) { json.rest[0].security = {}; } } catch (e) { diff --git a/src/wellKnownOIDCConfiguration.js b/src/wellKnownOIDCConfiguration.js index 764a5d51..053b03d9 100644 --- a/src/wellKnownOIDCConfiguration.js +++ b/src/wellKnownOIDCConfiguration.js @@ -7,20 +7,25 @@ module.exports = (req, res) => { const json = { - // REQUIRED. URL using the https scheme with no query or fragment component that the OP asserts as - // its Issuer Identifier. If Issuer discovery is supported (see Section 2), this value MUST be identical - // to the issuer value returned by WebFinger. This also MUST be identical to the iss Claim value in - // ID Tokens issued from this Issuer. + // REQUIRED. URL using the https scheme with no query or fragment + // component that the OP asserts as its Issuer Identifier. If Issuer + // discovery is supported (see Section 2), this value MUST be identical + // to the issuer value returned by WebFinger. This also MUST be + // identical to the iss Claim value in ID Tokens issued from this Issuer. issuer: `${config.baseUrl}`, - // REQUIRED. URL of the OPs JSON Web Key Set [JWK] document. This contains the signing key(s) the RP uses - // to validate signatures from the OP. The JWK Set MAY also contain the Server's encryption key(s), - // which are used by RPs to encrypt requests to the Server. When both signing and encryption keys are made - // available, a use (Key Use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate - // each keys intended usage. Although some algorithms allow the same key to be used for both signatures and - // encryption, doing so is NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be used to provide - // X.509 representations of keys provided. When used, the bare key values MUST still be present and MUST match - // those in the certificate. + // REQUIRED. URL of the OPs JSON Web Key Set [JWK] document. This + // contains the signing key(s) the RP uses to validate signatures from + // the OP. The JWK Set MAY also contain the Server's encryption key(s), + // which are used by RPs to encrypt requests to the Server. When both + // signing and encryption keys are made available, a use (Key Use) + // parameter value is REQUIRED for all keys in the referenced JWK Set to + // indicate each keys intended usage. Although some algorithms allow the + // same key to be used for both signatures and encryption, doing so is + // NOT RECOMMENDED, as it is less secure. The JWK x5c parameter MAY be + // used to provide X.509 representations of keys provided. When used, + // the bare key values MUST still be present and MUST match those in the + // certificate. jwks_uri: `${config.baseUrl}/keys`, // REQUIRED, URL to the OAuth2 authorization endpoint. @@ -32,8 +37,9 @@ module.exports = (req, res) => { // OPTIONAL, URL of the authorization server's introspection endpoint. introspection_endpoint: `${prefix}/introspect`, - // REQUIRED. JSON array containing a list of the Subject Identifier types that this OP supports. Valid types include pairwise and public. - "subject_types_supported": [ + // REQUIRED. JSON array containing a list of the Subject Identifier + // types that this OP supports. Valid types include pairwise and public. + subject_types_supported: [ "public" ] diff --git a/tests/api.js b/tests/api.js deleted file mode 100644 index 427142b1..00000000 --- a/tests/api.js +++ /dev/null @@ -1,1130 +0,0 @@ - -const Request = require('request'); -const request = require('supertest'); -const app = require("../src/index.js"); -const config = require("../src/config"); -const jwt = require("jsonwebtoken"); -const Url = require("url"); -const jwkToPem = require("jwk-to-pem"); -const Codec = require("../static/codec.js"); -const base64url = require("base64-url"); -const Lib = require("../src/lib"); -const crypto = require("crypto"); - - -const ENABLE_FHIR_VERSION_2 = true; -const ENABLE_FHIR_VERSION_3 = true; -const PREFERRED_FHIR_VERSION = ENABLE_FHIR_VERSION_3 ? "r3" : "r2"; -const PUBLIC_KEY = "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFyTmx6RlFwbzRIZWY5dkVPYkdPUQpqc0RtelhyWFY4aDd3bVFxaFdhRkh0cCtLZW14ZmplOU02YkNrdjFsQ2RkajNFU3ZqeTkrd3lHTFlXQXJIdFYrCitGVVA5NjJPTVI5L2lNakpGZ0RDQjM5bnY0MGZLaTJBajRseCt6cE1XUnJZdVN3ZWdiYnNtT0hrL2t5RXUrSlgKTGgzNDlOZlZQSGdaRlhNUno5bUhXNk9hT21PVEVVYlY1RWJ0TnIxUFpCQVNYSGhpZ3VBTXZGcFl5Z2I3blFzQgo4OTBUOXVWcmM4bDB1OWpwc2J2OU10ZXZFeGZCTFlGSDQ4LzFrOUovWk5aalhBY281U2E3QTFlVXZGaSt0b01oCnBPL0lpVCs0L1BQaVRmYU9naXkrd3piNEFhUnF0MVMvWWx5RExIeVJuV3hlNThkMTdWZGY0Y0EzclVpVll0ZEEKWVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="; -const PRIVATE_KEY = "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBck5sekZRcG80SGVmOXZFT2JHT1Fqc0RtelhyWFY4aDd3bVFxaFdhRkh0cCtLZW14CmZqZTlNNmJDa3YxbENkZGozRVN2ank5K3d5R0xZV0FySHRWKytGVVA5NjJPTVI5L2lNakpGZ0RDQjM5bnY0MGYKS2kyQWo0bHgrenBNV1JyWXVTd2VnYmJzbU9Iay9reUV1K0pYTGgzNDlOZlZQSGdaRlhNUno5bUhXNk9hT21PVApFVWJWNUVidE5yMVBaQkFTWEhoaWd1QU12RnBZeWdiN25Rc0I4OTBUOXVWcmM4bDB1OWpwc2J2OU10ZXZFeGZCCkxZRkg0OC8xazlKL1pOWmpYQWNvNVNhN0ExZVV2RmkrdG9NaHBPL0lpVCs0L1BQaVRmYU9naXkrd3piNEFhUnEKdDFTL1lseURMSHlSbld4ZTU4ZDE3VmRmNGNBM3JVaVZZdGRBWVFJREFRQUJBb0lCQVFDakRTVlFUZGVORjR0ZwpxUmlRQ29RTkJjOHpPcFAxRFB3aDdkZG1xOFVieThTRHlSMVVFVVI3ZXUzRk54K2UzdjRtaE95UFI2QnVkakJECkZUTFlEVkdPOUw3eFIxa0E0ZE91dHFscUJpRUNiWjd5eFM4RzNKR1AxWG9lSVdwd0M3RXhUSHNpcGVvZWRjbE0KVWVaTVRrRXJFYjhOU0tTd1BDSjlaMlVBQ3hWeXpSN3FraE5hVUZYRGNYb2U5Y2FiK3hCRWFJZkFSUUFodllBVwo3ZTlraldjbzRaRTREWm1HL1JnYnRkR1cxZ05xcERiWnE2cDR4VThIUHdoWWVPVko0dmgwUEZHVEp2VWJPTUx5CjU0dEx5a2lFc0lnQzYzRXZ1aTdmQzByWmRiVEt3Z293dnZvWC9uVXhua1VUQ25sREZtUnFReitCVURLZkVlcWIKRyt4V0NWYUJBb0dCQU5mRDBvN0dIRlhKRDBLMlRuSFdHVWhEdjdTMnJBS2lncHZrdVlqNkFSV3NqckJ1VWpGTgpzSGNnRW52cHRFZloyb2lRZjh0eTNCMG9RdnNsYUxwaTBTOG9uZGVISWI2Sms5SU5xRzJzeE5DdjAxdnozam1GCkk4aGFUajRwSzBpQ3lNWnVISUhWSHFQdUZWOStvSHc1QWVkbytSUjlRZ2NHMEdUTEduYWhHOENKQW9HQkFNMFUKN1lBYkJFVm4wY0JoYU9OREFhVmFLMlBZaGdjc3BOOGJNVDh1QlhYU2VzTXkyS3dRS0EwQVNLbm5vcUNoeThyeQpVbUFRN1NNUHM4V25OcEo4Ukh2cXBOazNiSVEyMnpLSHNBQkNmZ29pcWtPc2FqZmYvQWk0Y1E3UGMwYzV0LzA5CmRnOFJNMEw3cmhBdndRUnFraHNST09UemZocTRONVE0Qnd4NGV4c1pBb0dCQU1FU1JaUGt5dTRvb0RNK0Z5dmUKUFhsZ3htYmJIMGlzU3R0YzdIa1ozV2FicG9OUjlOS1JobHJTcERlbGhPRFduS3FmUXZ1MnFDaWZJbkRCcE5sRQpHNU5yY1BLdnhRNU81YXVNOVM1Tzd6OGVWcTl0cFdrckxqM1dNVFdHZVdqRlB3dnc5Q2xwbjZWcElrNzFiSDQ4Ck5PdnlEeEM2bFI3Y2hoWHJlSjYydzdLaEFvR0FTUHI3a2EwTGxnOWVDMUllMjFFTEV1YkZyaUJ0Z2J3WFovWHIKVG9wNEV2ZTJEQ1RhQ2xFdGo0TGNXT28vYTE1b2dXNCtka1ZQdmp4bVF4NUFRMXpKbWpka05wQ01vM2hLQk85WQphSjlBN3lacTVPNUVWbUgwOUwxK0xrRVF5dlgxVGI5RGRoVXU0dFZobWcwRWFTZnJtb3BFYnVWZnFPNkppTXR2ClpyYXhTSEVDZ1lBcW5EZ0Q3bW53bnFGdmJ0R2JTUjRUdVJzYi9TQi9HejArSzNtdDhRME5NbWNIVDFsM0c1REEKWDEvU3hZdUxLZ1F5UWNPQUh2b3RxcUp0R3I2d0ZaOHJVdnIwTnEwQUtFa2JyODF2V2NpbkhySWFySkZUaVE5Vgp2SGNxYktoRlJ5dEtndjdZcEt1STJSUWM5dG9FNS9BbDZBejJOdlJZOGJ2MzN3QUFxTTl1OXc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo="; - - - -//////////////////////////////////////////////////////////////////////////////// - -/** - * Promisified version of request. Rejects with an Error or resolves with the - * response (use response.body to access the parsed body). - * @param {Object} options The request options - * @return {Promise} - */ -function requestPromise(options) { - return new Promise((resolve, reject) => { - Request( - Object.assign({}, options, { strictSSL: false }), - (error, res, body) => { - if (error) { - return reject(error); - } - resolve(res); - } - ); - }); -} - -/** - * Throws an error if the response does not have the specified status code. - * @param {Object} res Response - * @param {Number} code The expected status code - * @returns {Object} Returns the response object to simplify promise chains - * @throws {Error} - */ -function expectStatusCode(res, code) { - if (res.statusCode !== code) { - throw new Error(`Expecting status code of ${code} but received ${res.statusCode}`); - } - return res; -} - -/** - * Throws an error if the response does not have the specified header. - * The header name is required and the value is optional. - * @param {Object} res The response - * @param {String} name The name of the header - * @param {String|RegExp} [value] The header value (checks for presence only if - * this is missing) - * @returns {Object} Returns the response object to simplify promise chains - * @throws {Error} - */ -function expectResponseHeader(res, name, value = null) { - let header = String(res.headers[name.toLowerCase()] || ""); - if (!header) { - throw new Error(`Expecting ${name} response header but it wasn't sent`); - } - if (value) { - if (value instanceof RegExp) { - if (!value.test(header)) { - throw new Error(`The ${name} response header "${header}" did not match the specified RegExp`); - } - } - else if (header !== value) { - throw new Error(`Expecting ${name} response header to equal ${value} but found ${header}`); - } - } - return res; -} - -function lookupOidcKeys(done) { - const path = `${config.baseUrl}/.well-known/openid-configuration/`; - let keysLocation, keys; - - Request({ - url: path, - json: true, - strictSSL: false - }, (error, res, body) => { - if (error) { - return done(error) - } - - try { - expectStatusCode(res, 200); - expectResponseHeader(res, 'Content-Type', /^application\/json/); - if (!body) { - throw new Error(`${path} did not return a JSON`); - } - if (!body.jwks_uri) { - throw new Error(`${path} did not return proper keys location`); - } - } catch(ex) { - return done(ex); - } - - Request({ - url: body.jwks_uri, - json: true, - strictSSL: false - }, (error2, res2, body2) => { - if (error2) { - return done(error2) - } - - try { - expectStatusCode(res2, 200); - expectResponseHeader(res, 'Content-Type', /^application\/json/); - if (!body2) { - throw new Error(`${keysLocation} did not return a JSON`) - } - if (!body2.keys) { - throw new Error(`${keysLocation} did not return keys`); - } - } catch(ex) { - return done(ex); - } - - done(null, body2.keys); - }) - }); -} - -/** - * Encodes the object with our proprietary codec to make it smaller. The - * serializes as JSON and returns a base64 version of it. - * @param {Object} object The object to encode - * @returns {String} - */ -function encodeSim(object = {}) { - return new Buffer( - JSON.stringify(Codec.encode(object)) - ).toString("base64"); -} - -/** - * Makes the initial authorization request and expects the server to redirect - * back to redirect_uri with the given state and a code. - * @param {Object} options - * @param {String} options.patient 0 or more comma-separated patient IDs. - * Defaults to "x" because we ignore it. - * @param {String} options.client_id The client_id of the app. Defaults to "x" - * because we ignore it. - * @param {String} options.redirect_uri The uri to redirect to. Defaults to - * "http://x.y" because we ignore it but still require it to be valid URL. - * @param {String} options.scope - * @param {String} options.state - * @param {String} options.aud - * @param {Object} options.launch - * @param {Number} options.launch.launch_pt 1 or 0 - * @param {Number} options.launch.skip_login 1 or 0 - * @param {Number} options.launch.skip_auth 1 or 0 - * @param {String} options.launch.patient - * @param {String} options.launch.encounter - * @param {String} options.launch.auth_error - * @returns {Promise} Returns a promise resolved with the code - */ -function getAuthCode(options) { - return new Promise((resolve, reject) => { - Request({ - url : `${options.baseUrl}auth/authorize`, - strictSSL: false, - followRedirect: false, - qs: { - response_type: "code", - patient : options.patient || "x", - client_id : options.client_id || "x", - redirect_uri : options.redirect_uri || "http://x.y", - scope : options.scope || "x", - state : options.state || "x", - launch : encodeSim(options.launch), - aud : `${options.baseUrl}fhir` - } - }, (error, res, body) => { - if (error) { - return reject(error) - } - - try { - expectStatusCode(res, 302); - - if (!res.headers.location) { - throw new Error(`auth/authorize did not redirect to the redirect_uri`) - } - let url = Url.parse(res.headers.location, true); - let code = url.query.code; - if (!code) { - console.log(res.headers) - throw new Error(`auth/authorize did not redirect to the redirect_uri with code parameter`) - } - // console.log("code: ", JSON.parse(base64url.decode(code.split(".")[1]))); - resolve(code); - } catch(ex) { - reject(ex); - } - }); - }); -} - -function getAuthToken(options) { - return new Promise((resolve, reject) => { - Request({ - url : `${options.baseUrl}auth/token`, - method : "POST", - strictSSL: false, - followRedirect: false, - json: true, - form: { - grant_type: "authorization_code", - code : options.code - } - }, (error, res, body) => { - if (error) { - return reject(error) - } - - try { - expectStatusCode(res, 200); - resolve(body); - } catch(ex) { - reject(ex); - } - }); - }); -} - -/** - * Posts a refresh token to the token endpoint and resolves with the "new" - * token response. - * @param {Object} options - * @param {String} options.baseUrl - * @param {String} options.refreshToken - * @returns {Promise} - */ -function refreshSession(options) { - return requestPromise({ - url : `${options.baseUrl}auth/token`, - method : "POST", - followRedirect: false, - json : true, - form: { - grant_type : "refresh_token", - refresh_token: options.refreshToken - } - }).then(res => res.body); -} - -function authorize(options) { - return getAuthCode(options).then( - code => getAuthToken({ code, baseUrl: options.baseUrl }) - ); -} - -function buildRoutePermutations(suffix = "", fhirVersion) { - suffix = suffix.replace(/^\//, ""); - let out = []; - - if (ENABLE_FHIR_VERSION_3 && (!fhirVersion || fhirVersion == 3)) { - out.push( - `/v/r3/sim/whatever/${suffix}`, - `/v/r3/${suffix}` - ); - } - - if (ENABLE_FHIR_VERSION_2 && (!fhirVersion || fhirVersion == 2)) { - out.push( - `/v/r2/sim/whatever/${suffix}`, - `/v/r2/${suffix}` - ); - } - - return out; -} - -function decodeJwtToken(token) { - return JSON.parse( - new Buffer(token.split(".")[1], "base64").toString("utf8") - ); -} -//////////////////////////////////////////////////////////////////////////////// -let server; -before(() => { - server = app.listen(config.port); -}); - -after(() => { - server.close(); -}); - - -describe('index', function() { - it('responds with html', function(done) { - request(app) - .get('/') - .expect('Content-Type', /html/) - .expect(200, done); - }); -}); - -["picker", "login", "authorize"].forEach(endPoint => { - describe(endPoint, function() { - buildRoutePermutations(endPoint).forEach(path => { - describe(`GET ${path}`, function() { - it('responds with html', function(done) { - request(app) - .get(path) - .expect('Content-Type', /html/) - .expect(200, done); - }); - }); - }); - }); -}); - -describe('Proxy', function() { - this.timeout(10000); - buildRoutePermutations("fhir/metadata").forEach(path => { - it(path + ' responds with html in browsers', done => { - request(app) - .get(path) - .set('Accept', 'text/html') - .expect('Content-Type', /^text\/html/) - .expect(200, done); - }); - - // TODO: Support XML - // it(path + ' responds with xml if requested', done => { - // request(app) - // .get(path) - // .set('Accept', 'application/xml') - // .expect('Content-Type', /^application\/xml\+fhir/) - // .expect(200, done); - // }); - }); - - it ("Validates the fhir version", done => { - request(app) - .get("/v/r300/fhir/metadata") - .expect('Content-Type', /json/) - .expect('{"error":"FHIR server r300 not found"}') - .expect(400, done); - }); - - it ("If auth token is sent - validates it", done => { - request(app) - .get(`/v/${PREFERRED_FHIR_VERSION}/fhir/metadata`) - .set("authorization", "Bearer whatever") - .expect('Content-Type', /text/) - .expect("JsonWebTokenError: jwt malformed") - .expect(401, done); - }); - - it ("Can simulate custom token errors"); - it ("Keeps protected data-sets read-only"); - it ("Make urls conditional and if exists, change /id to ?_id="); - it ("Apply patient scope to GET requests"); - - it ("Adjust urls in the fhir response", done => { - request(app) - .get("/v/" + PREFERRED_FHIR_VERSION + "/fhir/Patient") - .expect(res => { - // FIXME: version mismatch... - if (res.text.indexOf(config.fhirServerR3) > -1) { - throw new Error("Not all URLs replaced"); - } - }) - .end(done); - }); - - buildRoutePermutations("fhir/metadata").forEach(path => { - it(path + ' inject the SMART information in metadata responses', done => { - request(app) - .get(path) - .expect('Content-Type', /\bjson\b/i) - .expect(200) - .expect(res => { - let uris = Lib.getPath(res.body, "rest.0.security.extension.0.extension"); - - // authorize --------------------------------------------------- - let authorizeCfg = uris.find(o => o.url == "authorize"); - if (!authorizeCfg) { - throw new Error("No 'authorize' endpoint found in the conformance statement"); - } - if (authorizeCfg.valueUri != config.baseUrl + path.replace("fhir/metadata", "auth/authorize")) { - throw new Error("Wrong 'authorize' endpoint found in the conformance statement"); - } - - // token ------------------------------------------------------- - let tokenCfg = uris.find(o => o.url == "token"); - if (!tokenCfg) { - throw new Error("No 'token' endpoint found in the conformance statement"); - } - if (tokenCfg.valueUri != config.baseUrl + path.replace("fhir/metadata", "auth/token")) { - throw new Error("Wrong 'token' endpoint found in the conformance statement"); - } - - // register ---------------------------------------------------- - // TODO: Un-comment when we support DCR - // let registerCfg = uris.find(o => o.url == "register"); - // if (!registerCfg) { - // throw new Error("No 'register' endpoint found in the conformance statement"); - // } - // if (registerCfg.valueUri != config.baseUrl + path.replace("fhir/metadata", "auth/register")) { - // throw new Error("Wrong 'register' endpoint found in the conformance statement"); - // } - }) - .end(done); - }); - }); - - it ("pull the resource out of the bundle if we converted a /id url into a ?_id= query", done => { - let patientID; - - // We cannot know any IDs but we need to use one for this test, thus - // query all the patients with _count=1 to find the first one and use - // it's ID. - return requestPromise({ - uri: `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/fhir/Patient`, - json: true, - qs: { - _count: 1 - } - }) - - // Use the first patient ID - .then(res => res.body.entry[0].resource.id) - - // Now do another request with that ID - .then(id => { - patientID = id; - return requestPromise({ - uri: `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/fhir/Patient/${id}`, - json: true - }); - }) - - // Did we get a patient with the requested id? - .then(res => { - return res.body.resourceType == "Patient" && res.body.id == patientID ? - Promise.resolve() : - Promise.reject("No patient returned by id") - }) - - // complete - .then(() => done(), done); - }); - - it ("Pretty print if called from a browser"); - - if (ENABLE_FHIR_VERSION_3) { - it ("Replies with application/fhir+json for STU3", done => { - request(app) - .get(`/v/${PREFERRED_FHIR_VERSION}/fhir/Patient`) - .expect("content-Type", /^application\/fhir\+json/i) - .end(done); - }); - } - - if (ENABLE_FHIR_VERSION_2) { - it ("Replies with application/json+fhir for DSTU2", done => { - request(app) - .get("/v/r2/fhir/Patient") - .expect("content-Type", /^application\/json\+fhir/i) - .expect(/\n.+/, done); - }); - } - - it ("Replies with formatted JSON for bundles", done => { - request(app).get(`/v/${PREFERRED_FHIR_VERSION}/fhir/Patient`).expect(/\n.+/, done); - }); - - it ("Replies with formatted JSON for single resources", done => { - request(app).get(`/v/${PREFERRED_FHIR_VERSION}/fhir/Observation/smart-5328-height`).expect(/\n.+/, done); - }); - - it ("Handles pagination", done => { - request(app) - .get(`/v/${PREFERRED_FHIR_VERSION}/fhir/Patient`) - .expect(res => { - if (!Array.isArray(res.body.link)) { - throw new Error("No links found"); - } - - let next = res.body.link.find(l => l.relation == "next") - if (!next) { - throw new Error("No next link found"); - } - // console.log(next) - return request(app).get(next.url).expect(res2 => { - if (!Array.isArray(res.body.link)) { - throw new Error("No links found on second page"); - } - - let self = res.body.link.find(l => l.relation == "self") - if (!self) { - throw new Error("No self link found on second page"); - } - if (self.url !== next.url) { - throw new Error("Links mismatch"); - } - - let next2 = res.body.link.find(l => l.relation == "next") - if (!next2) { - throw new Error("No next link found on second page"); - } - - console.log(next2) - }) - }) - .end(done); - }); -}); - -describe('Auth', function() { - describe('authorize', function() { - - // auth/authorize Checks for required params - buildRoutePermutations("auth/authorize").forEach(path => { - let query = []; - [ - "response_type", - "client_id", - "redirect_uri", - "scope", - "state", - "aud" - ].forEach(name => { - it(`${path} requires "${name}" param`, done => { - request(app) - .get(path + "?" + query.join("&")) - .expect(400) - .expect(`Missing ${name} parameter`) - .end(() => { - query.push(name + "=" + (name == "redirect_uri" ? "http%3A%2F%2Fx" : "x")); - done(); - }); - }); - }); - - // it(`${path} with missing "client_id" param`, done => { - // request(app) - // .get(path + "?response_type=code&client_id=&redirect_uri=http%3A%2F%2Fx&aud=x&state=abc") - // .end(function(error, res) { - // console.log(res.body, res.headers) - // done(); - // }); - // }); - }); - - // auth/authorize validates the redirect_uri parameter - buildRoutePermutations("auth/authorize").forEach(path => { - it(`${path} - validates the redirect_uri parameter`, done => { - request(app) - .get(path + "?response_type=x&client_id=x&redirect_uri=x&scope=x&state=x&aud=x") - .expect(/^Invalid redirect_uri parameter/) - .expect(400) - .end(done); - }); - }); - - // can simulate invalid redirect_uri error - { - let sim = new Buffer('{"auth_error":"auth_invalid_redirect_uri"}').toString('base64'); - let paths = buildRoutePermutations("auth/authorize?launch=" + sim + "&response_type=x&client_id=x&redirect_uri=http%3A%2F%2Fx&scope=x&state=x&aud=x"); - paths.push(`/v/${PREFERRED_FHIR_VERSION}/sim/${sim}/auth/authorize?response_type=x&client_id=x&redirect_uri=http%3A%2F%2Fx&scope=x&state=x&aud=x"`); - paths.forEach(path => { - it (path.split("?")[0] + " can simulate invalid redirect_uri error", done => { - request(app) - .get(path) - .expect(302) - .expect(function(res) { - if (!res.headers.location || res.headers.location.indexOf("error=sim_invalid_redirect_uri") == -1) { - throw new Error(`No error passed to the redirect ${res.headers.location}`) - } - }) - .end(done); - }); - }); - } - - // can simulate invalid scope error - { - let sim = new Buffer('{"auth_error":"auth_invalid_scope"}').toString('base64'); - let paths = buildRoutePermutations("auth/authorize?launch=" + sim + "&response_type=x&client_id=x&redirect_uri=http%3A%2F%2Fx&scope=x&state=x&aud=x"); - paths.push(`/v/${PREFERRED_FHIR_VERSION}/sim/${sim}/auth/authorize?response_type=x&client_id=x&redirect_uri=http%3A%2F%2Fx&scope=x&state=x&aud=x"`); - paths.forEach(path => { - it (path.split("?")[0] + " can simulate invalid scope error", done => { - request(app) - .get(path) - .expect(302) - .expect(function(res) { - if (!res.headers.location) { - throw new Error(`No redirect`) - } - let url = Url.parse(res.headers.location, true); - if (url.query.error != "sim_invalid_scope") { - throw new Error(`Wrong redirect ${res.headers.location}`) - } - }) - .end(done); - }); - }); - } - - // can simulate invalid client_id error - { - let sim = new Buffer('{"auth_error":"auth_invalid_client_id"}').toString('base64'); - let paths = buildRoutePermutations("auth/authorize?launch=" + sim + "&response_type=x&client_id=x&redirect_uri=http%3A%2F%2Fx&scope=x&state=x&aud=x"); - paths.push(`/v/${PREFERRED_FHIR_VERSION}/sim/${sim}/auth/authorize?response_type=x&client_id=x&redirect_uri=http%3A%2F%2Fx&scope=x&state=x&aud=x"`); - paths.forEach(path => { - it (path.split("?")[0] + " can simulate invalid client_id error", done => { - request(app) - .get(path) - .expect(302) - .expect(function(res) { - if (!res.headers.location) { - throw new Error(`No redirect`) - } - let url = Url.parse(res.headers.location, true); - if (url.query.error != "sim_invalid_client_id") { - throw new Error(`Wrong redirect ${res.headers.location}`) - } - }) - .end(done); - }); - }); - } - - // rejects invalid audience value - buildRoutePermutations( - "auth/authorize?response_type=x&client_id=x&redirect_uri=http%3A%2F%2Fx&scope=x&state=x&launch=0&aud=whatever" + - encodeURIComponent(config.fhirServerR2), - 2 - ).forEach(path => { - it (path.split("?")[0] + " rejects invalid audience value", done => { - request(app) - .get(path) - .expect(302) - .expect(function(res) { - if (!res.headers.location) { - throw new Error(`No redirect`) - } - let url = Url.parse(res.headers.location, true); - if (url.query.error != "bad_audience") { - throw new Error(`Wrong redirect ${res.headers.location}`) - } - }) - .end(done); - }); - }); - - // can show encounter picker - buildRoutePermutations( - "auth/authorize" + - "?client_id=x" + - "&response_type=code" + - "&scope=patient%2F*.read%20launch" + - "&redirect_uri=https%3A%2F%2Fsb-apps.smarthealthit.org%2Fapps%2Fgrowth-chart%2F" + - "&state=x" + - "&login_success=1" + - "&patient=fb48de1b-e485-458a-ac0f-c5a54c26b58d" - ).forEach(path => { - let aud = encodeURIComponent(config.baseUrl + path.split("auth/authorize")[0] + "fhir"); - let launch = new Buffer(JSON.stringify({ - launch_ehr : 1, - select_encounter: 1 - })).toString("base64"); - let fullPath = path + "&aud=" + aud + "&launch=" + launch; - - it (path.split("?")[0] + " can show encounter picker", done => { - request(app) - .get(fullPath) - .expect(302) - .expect(function(res) { - if (!res.headers.location || res.headers.location.indexOf(fullPath.replace(/\/auth\/authorize\?.*/, "/encounter?")) !== 0) { - throw new Error(`Wrong redirect ${res.headers.location}`) - } - }) - .end(done); - }); - }); - }); - - describe('Confidential Clients', function() { - buildRoutePermutations("auth/token").forEach(path => { - let token = jwt.sign("whatever", config.jwtSecret); - - it (`${path} - can simulate auth_invalid_client_secret`, done => { - request(app) - .post(path) - .type('form') - .set("Authorization", "Basic bXktYXBwOm15LWFwcC1zZWNyZXQtMTIz") - .send({ - grant_type: "refresh_token", - auth_error: "auth_invalid_client_secret", - refresh_token: token - }) - .expect("Simulated invalid client secret error") - .expect(401) - .end(done); - }); - - it (`${path} - rejects empty auth header`, done => { - request(app) - .post(path) - .type('form') - .set("Authorization", "Basic") - .send({ - grant_type: "refresh_token", - refresh_token: token - }) - .expect("The authorization header 'Basic' cannot be empty") - .expect(401) - .end(done); - }); - - it (`${path} - rejects invalid auth header`, done => { - request(app) - .post(path) - .type('form') - .set("Authorization", "Basic bXktYXB") - .send({ - grant_type: "refresh_token", - refresh_token: token - }) - .expect(/^Bad authorization header/) - .expect(401) - .end(done); - }); - - it (`${path} - can simulate sim_invalid_token`, done => { - let code = jwt.sign({ auth_error:"token_invalid_token" }, config.jwtSecret); - request(app) - .post(path) - .type('form') - .set("Authorization", "Basic bXktYXBwOm15LWFwcC1zZWNyZXQtMTIz") - .send({ - grant_type: "authorization_code", - code - }) - .expect("Simulated invalid token error") - .expect(401) - .end(done); - }); - }); - }); - - describe('OIDC signature algorithm works correctly', function() { - buildRoutePermutations().forEach(path => { - let code, idToken, key, keysLocation; - let aud = config.baseUrl + path + "fhir"; - let launch = encodeSim({ - launch_pt : 1, - skip_login: 1, - skip_auth : 1, - patient : "abc", - encounter : "bcd" - }); - it(`${path}auth/authorize - generates a code`, done => { - // getAuthCode({ - // client_id: "x", - // patient: "x" - // }); - - request(app) - .get(`${path}auth/authorize?response_type=code&launch=${launch}` + - `&patient=abc&client_id=x&redirect_uri=${encodeURIComponent("http://x.y")}` + - `&scope=profile%20openid%20launch&state=x&aud=${encodeURIComponent(aud)}`) - .expect(302) - .expect(function(res) { - if (!res.headers.location) { - throw new Error(`auth/authorize did not redirect to the redirect_uri`) - } - let url = Url.parse(res.headers.location, true); - code = url.query.code; - if (!code) { - console.log(res.headers) - throw new Error(`auth/authorize did not redirect to the redirect_uri with code parameter`) - } - // console.info("code: ", JSON.parse(Buffer.from(code.split(".")[1], 'base64').toString())); - }) - .end(done); - }); - - it(`${path}auth/token - provides id_token`, done => { - getAuthToken({ code, baseUrl: config.baseUrl + path }).then(body => { - if (!body || !body.id_token) { - return done(new Error(`auth/token did not return id_token`)); - } - idToken = body.id_token; - done(); - }, done); - }); - - it(`the access token can be verified`, done => { - lookupOidcKeys((error, keys) => { - if (error) { - return done(error); - } - key = keys[0] - try { - jwt.verify(idToken, jwkToPem(key), { algorithms: ["RS256"] }) - done() - } catch (ex) { - done(ex) - } - }); - }); - }); - }); - - describe('token', function() { - buildRoutePermutations().forEach(path => { - it(`${path}auth/token can simulate sim_expired_refresh_token`, done => { - authorize({ - scope : "offline_access", - baseUrl: config.baseUrl + path, - launch : { - launch_pt : 1, - skip_login: 1, - skip_auth : 1, - patient : "abc", - encounter : "bcd", - auth_error: "token_expired_refresh_token" - } - }) - .then(tokenResponse => { - return refreshSession({ - baseUrl: config.baseUrl + path, - accessToken: tokenResponse.access_token, - refreshToken: tokenResponse.refresh_token - }); - }) - .then(result => { - if (result != config.errors.sim_expired_refresh_token) { - return done(new Error("No sim_expired_refresh_token error returned")); - } - done(); - }, done); - }); - }); - }); -}); - -describe('Generator', () => { - describe('RSA Generator', () => { - it ("can generate random RSA-256 key pairs", done => { - let privateKey, publicKey; - - function ketKeyPair() { - return requestPromise({ - uri: `${config.baseUrl}/generator/rsa`, - json: true, - qs: { - enc: "base64" - } - }) - .then(res => expectStatusCode(res, 200)) - .then(res => expectResponseHeader(res, 'Content-Type', /\bjson\b/i)) - .then(res => { - let { privateKey, publicKey } = res.body; - if (!privateKey) { - return Promise.reject( - new Error("The generator did not create a private key") - ); - } - if (!publicKey) { - return Promise.reject( - Error("The generator did not create a public key") - ); - } - return { privateKey, publicKey }; - }); - } - - ketKeyPair() - .then(keys => { - privateKey = keys.privateKey; - publicKey = keys.publicKey; - return ketKeyPair(); - }) - .then(keys => { - if (keys.privateKey == privateKey) { - throw new Error("privateKey does not change between requests"); - } - if (keys.publicKey == publicKey) { - throw new Error("publicKey does not change between requests"); - } - done(); - }) - .catch(done); - }); - }); -}); - -describe('Backend Services', () => { - - - describe('Client Registration', () => { - - it ("requires form-urlencoded POST", done => { - return requestPromise({ - method: "POST", - uri: `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/register` - }) - .then(res => expectStatusCode(res, 401)) - .then(res => { - if (res.body != "Invalid request content-type header (must be 'application/x-www-form-urlencoded')") { - throw new Error("Did not return the proper error message"); - } - return res; - }) - .then(res => done(), done) - }); - - it ("requires 'iss' parameter", done => { - return requestPromise({ - method: "POST", - uri : `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/register`, - form : {} - }) - .then(res => expectStatusCode(res, 400)) - .then(res => { - if (res.body != "Missing iss parameter") { - throw new Error("Did not return the proper error message"); - } - return res; - }) - .then(res => done(), done) - }); - - it ("requires 'pub_key' parameter", done => { - return requestPromise({ - method: "POST", - uri : `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/register`, - form : { - iss: "whatever" - } - }) - .then(res => expectStatusCode(res, 400)) - .then(res => { - if (res.body != "Missing pub_key parameter") { - throw new Error("Did not return the proper error message"); - } - return res; - }) - .then(res => done(), done) - }); - - it ("validates the 'dur' parameter", done => { - return Promise.all( - [ - "x", - Infinity, - -Infinity, - -2 - ].map(dur => { - return requestPromise({ - method: "POST", - uri : `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/register`, - form : { - iss: "whatever", - pub_key: "abc", - dur - } - }) - .then(res => expectStatusCode(res, 400)) - .then(res => { - if (res.body != "Invalid dur parameter") { - throw new Error("Did not return the proper error message"); - } - return res; - }); - }) - ) - .then(res => done(), done) - }); - - it ("basic usage", done => { - return requestPromise({ - method: "POST", - uri : `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/register`, - form : { - iss : "whatever", - pub_key: "something" - } - }) - .then(res => expectStatusCode(res, 200)) - .then(res => expectResponseHeader(res, "content-type", /\btext\/plain\b/i)) - .then(res => { - if (res.body.split(".").length != 3) { - throw new Error("Did not return proper client id"); - } - return res; - }) - .then(res => done(), done) - }); - - it ("accepts custom duration", done => { - return requestPromise({ - method: "POST", - uri : `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/register`, - form : { - iss : "whatever", - pub_key: "something", - dur : 23 - } - }) - .then(res => expectStatusCode(res, 200)) - .then(res => expectResponseHeader(res, "content-type", /\btext\/plain\b/i)) - .then(res => { - if (res.body.split(".").length != 3) { - throw new Error("Did not return proper client id"); - } - let token = decodeJwtToken(res.body); - let exp = token.accessTokensExpireIn; - if (exp != 23) { - throw new Error( - `Expected "accessTokensExpireIn" property to equal 23 but found ${exp}` - ) - } - }) - .then(res => done(), done) - }); - it ("accepts custom simulated errors", done => { - const err = "test error" - return requestPromise({ - method: "POST", - uri : `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/register`, - form : { - iss : "whatever", - pub_key: "something", - auth_error: err - } - }) - .then(res => expectStatusCode(res, 200)) - .then(res => expectResponseHeader(res, "content-type", /\btext\/plain\b/i)) - .then(res => { - if (res.body.split(".").length != 3) { - throw new Error("Did not return proper client id"); - } - let token = decodeJwtToken(res.body); - let x = token.auth_error; - if (x !== err) { - throw new Error( - `Expected "auth_error" property to equal "${err}" but found "${x}"` - ) - } - }) - .then(res => done(), done) - }); - }); - - describe('Authorization Claim', () => { - it ("Works as expected", done => { - const iss = "whatever"; - const tokenUrl = `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/token`; - - return requestPromise({ - method: "POST", - uri : `${config.baseUrl}/v/${PREFERRED_FHIR_VERSION}/auth/register`, - form : { - iss, - pub_key: PUBLIC_KEY - } - }) - .then(res => { - let jwtToken = { - iss, - sub: res.body, - aud: tokenUrl, - exp: Date.now()/1000 + 300, // 5 min - jti: crypto.randomBytes(32).toString("hex") - }; - - return requestPromise({ - method: "POST", - url : tokenUrl, - json : true, - form : { - scope : "system/*.*", - grant_type: "client_credentials", - client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", - client_assertion: jwt.sign( - jwtToken, - base64url.decode(PRIVATE_KEY), - { algorithm: 'RS256'} - ) - } - }); - }) - .then(res => { - // console.log(res.body); - if (res.body.token_type !== "bearer") { - throw new Error(`Authorization failed! Expecting token_type: bearer but found ${res.body.token_type}`); - } - if (res.body.expires_in !== 900) { - throw new Error(`Authorization failed! Expecting expires_in: 900 but found ${res.body.expires_in}`); - } - if (!res.body.access_token) { - throw new Error(`Authorization failed! No access_token returned`); - } - if (res.body.access_token.split(".").length != 3) { - throw new Error("Did not return proper access_token"); - } - return res; - }) - .then(res => done(), done); - }); - }); - describe('Fhir Requests', () => { - it ("TODO..."); - }); -}); diff --git a/tests/e2e/config.js b/tests/e2e/config.js deleted file mode 100644 index 4ace5136..00000000 --- a/tests/e2e/config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - TIMEOUT : 15000, // timeout when waiting for something - PAUSE : 800, // pause between interactions so that humans can see wait is going on - PICKER_URL : "https://localhost:8443", -}; \ No newline at end of file diff --git a/tests/e2e/globals.js b/tests/e2e/globals.js deleted file mode 100644 index a73069ab..00000000 --- a/tests/e2e/globals.js +++ /dev/null @@ -1,10 +0,0 @@ -const TIMEOUT = 15000; // timeout when waiting for something -const PAUSE = 800; // pause between interactions so that humans can see wait is going on -const PICKER_URL = "https://localhost:8443"; - -module.exports = { - waitForConditionTimeout: TIMEOUT, - TIMEOUT, - PAUSE, - PICKER_URL -}; \ No newline at end of file diff --git a/tests/e2e/lib.js b/tests/e2e/lib.js deleted file mode 100644 index 4bdd6725..00000000 --- a/tests/e2e/lib.js +++ /dev/null @@ -1,24 +0,0 @@ -var CFG = require("./config.js"); - - -module.exports = function(browser) { - - function pause(n) { - browser.pause(n === undefined ? CFG.PAUSE : n); - } - - function setTextInputValue(selector, value) { - // browser.moveTo(selector, 0, 0); - // browser.click(selector); - pause(20); - browser.clearValue(selector); - pause(20); - browser.setValue(selector, value); - pause(20); - } - - return { - pause, - setTextInputValue - }; -}; \ No newline at end of file diff --git a/tests/e2e/spec/login-screen.js b/tests/e2e/spec/login-screen.js deleted file mode 100644 index 03489086..00000000 --- a/tests/e2e/spec/login-screen.js +++ /dev/null @@ -1,55 +0,0 @@ -const CFG = require("../config.js"); -const BASE = `${CFG.PICKER_URL}/v/r3/login`; - -// ?client_id=growth_chart -// &response_type=code -// &scope=patient%2F*.read%20launch -// &redirect_uri=https%3A%2F%2Fsb-apps.smarthealthit.org%2Fapps%2Fgrowth-chart%2F -// &state=3f6b2600-14d9-3775-26c2-015fd488b499 -// &aud= -// &launch=eyJsYXVuY2hfZWhyIjoiMSIsInBhdGllbnQiOiJmYjQ4ZGUxYi1lNDg1LTQ1OGEtYWMwZi1jNWE1NGMyNmI1OGQifQ -// &provider= -// &login_type=provider -// &aud_validated=1 - -exports["Renders select for providers and no provider selected"] = function(browser) { - browser.url(`${BASE}?login_type=provider`); - browser.waitForElementVisible("#user-id"); - browser.waitForElementNotVisible("#user-name"); - browser.end(); -}; - -exports["Renders select for providers and single provider selected"] = function(browser) { - browser.url(`${BASE}?login_type=provider&provider=smart-Practitioner-71032702`); - browser.waitForElementVisible("#user-id"); - browser.waitForElementNotVisible("#user-name"); - browser.end(); -}; - -exports["Renders select for providers and multiple providers selected"] = function(browser) { - browser.url(`${BASE}?login_type=provider&provider=smart-Practitioner-71032702%2Csmart-Practitioner-71614502`); - browser.waitForElementVisible("#user-id"); - browser.waitForElementNotVisible("#user-name"); - browser.end(); -}; - -exports["Renders text field in patient mode if no patients are passed"] = function(browser) { - browser.url(`${BASE}?login_type=patient&`); - browser.waitForElementVisible("#user-name"); - browser.waitForElementNotVisible("#user-id"); - browser.end(); -}; - -exports["Renders select in patient mode if multiple patients are passed"] = function(browser) { - browser.url(`${BASE}?login_type=patient&patient=fb48de1b-e485-458a-ac0f-c5a54c26b58d,de699b5d-d9f4-4208-868c-2ecd80c83ed2`); - browser.waitForElementVisible("#user-id"); - browser.waitForElementNotVisible("#user-name"); - browser.end(); -}; - -exports["Can hide it's navbar"] = function(browser) { - browser.url(`${BASE}?login_type=patient&hide_navbar=1`); - browser.waitForElementVisible("body"); - browser.waitForElementNotPresent(".navbar"); - browser.end(); -}; diff --git a/tests/e2e/spec/main.js b/tests/e2e/spec/main.js deleted file mode 100644 index 1a007175..00000000 --- a/tests/e2e/spec/main.js +++ /dev/null @@ -1,144 +0,0 @@ -const CFG = require("../config.js"); - -exports["FHIR Version"] = function(browser) { - [ "r2", "r3" ].forEach(name => { - browser.url(`${CFG.PICKER_URL}?fhir_version=${name}`); - browser.waitForElementVisible("body"); - browser.expect.element('#fhir-version').value.to.equal(name); - }); - browser.end(); -}; - -exports["Launch Type"] = function(browser) { - [ - { name: "launch_prov", id: "launch-prov" }, - { name: "launch_pt" , id: "launch-pt" }, - { name: "launch_ehr" , id: "launch-ehr" } - ].forEach(meta => { - browser.url(`${CFG.PICKER_URL}?${meta.name}=1`); - browser.waitForElementVisible("body"); - browser.expect.element('input[name="launch-type"]:checked').to.have.attribute("id").which.equals(meta.id); - }); - browser.end(); -}; - -exports["Simulate EHR"] = function(browser) { - - // Direct - browser.url(`${CFG.PICKER_URL}?sim_ehr=0`); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body.login-form"); - browser.closeWindow(); - browser.window_handles(result => browser.switchWindow(result.value[0])); - - // Simulate EHR - browser.url(`${CFG.PICKER_URL}?sim_ehr=1`); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body.ehr"); - browser.end(); -}; - -exports["Renders the user-login page if no providers are selected"] = function(browser) { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0`); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body.login-form"); - browser.expect.element("#login-type").text.to.equal("Practitioner"); - browser.end(); -}; - -exports["Renders the user-login page if multiple providers are selected"] = function(browser) { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0&fhir_version=r3&provider=smart-Practitioner-71032702%2Csmart-Practitioner-71614502`); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body.login-form"); - browser.expect.element("#login-type").text.to.equal("Practitioner"); - browser.end(); -}; - -exports["Does not render the user-login page if one provider is selected"] = function(browser) { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0&fhir_version=r3&provider=smart-Practitioner-71032702`); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body"); - browser.waitForElementNotPresent("body.login-form"); - browser.end(); -}; - -exports["Renders the patient picker if no patients are selected"] = function(browser) { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0&fhir_version=r3&provider=smart-Practitioner-71032702&patient=`); - browser.waitForElementVisible("body"); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body.patient-picker"); - browser.end(); -}; - -exports["Renders the patient picker if multiple patients are selected"] = function(browser) { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0&fhir_version=r3&provider=smart-Practitioner-71032702&patient=fb48de1b-e485-458a-ac0f-c5a54c26b58d,de699b5d-d9f4-4208-868c-2ecd80c83ed2`); - browser.waitForElementVisible("body"); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body.patient-picker"); - browser.end(); -}; - -exports["Does not render the patient picker if single patient is selected"] = function(browser) { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0&fhir_version=r3&provider=smart-Practitioner-71032702&patient=fb48de1b-e485-458a-ac0f-c5a54c26b58d`); - browser.waitForElementVisible("body"); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body"); - browser.waitForElementNotPresent("body.patient-picker"); - browser.end(); -}; - -exports["Can show the encounter selector"] = function(browser) { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0&fhir_version=r3&provider=smart-Practitioner-71032702&patient=fb48de1b-e485-458a-ac0f-c5a54c26b58d&select_encounter=1`); - browser.waitForElementVisible("body"); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementNotPresent("body.encounter-picker"); - browser.end(); -}; - -exports["Can skip the encounter selector"] = function(browser) { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0&fhir_version=r3&provider=smart-Practitioner-71032702&patient=fb48de1b-e485-458a-ac0f-c5a54c26b58d&select_encounter=0`); - browser.waitForElementVisible("body"); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body.encounter-picker"); - browser.waitForElementNotPresent("body.encounter-picker"); - browser.end(); -}; - -exports["Can simulate authentication errors"] = function(browser) { - [ - { - auth_error: "auth_invalid_client_id", - message: "Invalid client_id parameter" - }, - { - auth_error: "auth_invalid_redirect_uri", - message: "Invalid redirect_uri parameter" - }, - { - auth_error: "auth_invalid_scope", - message: "Invalid scope parameter" - } - ].forEach(error => { - browser.url(`${CFG.PICKER_URL}?sim_ehr=0&fhir_version=r3&auth_error=${error.auth_error}`); - browser.waitForElementVisible("body"); - browser.click("#ehr-launch-url"); - browser.window_handles(result => browser.switchWindow(result.value[1])); - browser.waitForElementPresent("body"); - browser.expect.element("body").text.to.equal(error.message); - browser.closeWindow(); - browser.window_handles(result => browser.switchWindow(result.value[0])); - }); - browser.end(); -}; - -exports['@disabled'] = false;