diff --git a/.github/workflows/test_tests.yml b/.github/workflows/test_tests.yml index c743e448..bbbfdf1d 100644 --- a/.github/workflows/test_tests.yml +++ b/.github/workflows/test_tests.yml @@ -34,6 +34,7 @@ jobs: SECRET_VAR1: secret-var1-value SECRET_VAR2: secret-var2-value SECRET_FOR_MISSING_FIELD: secret for missing field + _ORIGIN: github name: Run Puppeteer tests steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ddbba98..54d4ef78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ Format: - added additional functionality to the sign method to allow developers to take a name argument to sign on canvas: issue #596 - A new script: `alkiln-run`, which acts like `npm run cucumber`, but can be run in any directory, not just in an npm package. +- Additional environment variables and their validation to allow for tests that run on a developer's server/Playground instead of through GitHub. Also, other functionality for that purpose. [Issue #661](https://github.com/SuffolkLITLab/ALKiln/issues/661) +- Tests for new session_vars behavior and improve previous tests. ### Changed - upgraded cucumber v8.6.0 @@ -51,6 +53,7 @@ Format: - the github action no longer runs `npm run XYZ`; it directly calls scripts, e.g. `alkiln-setup`, `alkiln-run`, `alkiln-takedown` - don't print the ["publish this cucumber report" message](https://github.com/cucumber/cucumber-js/blob/main/docs/configuration.md#options) +- Adjusted validation of some environment variables to account for Playground vs. GitHub or local test runs. [Issue #661](https://github.com/SuffolkLITLab/ALKiln/issues/661) - Projects created in da each have a unique name. https://github.com/SuffolkLITLab/ALKiln/issues/663 - Project name prefix now includes ALKiln in it for clarity diff --git a/action.yml b/action.yml index edd3dc1a..3e0d824d 100644 --- a/action.yml +++ b/action.yml @@ -61,6 +61,7 @@ runs: echo "DOCASSEMBLE_DEVELOPER_API_KEY=${{ inputs.DOCASSEMBLE_DEVELOPER_API_KEY }}" >> $GITHUB_ENV echo "ALKILN_VERSION=${{ inputs.ALKILN_VERSION }}" >> $GITHUB_ENV echo "MAX_SECONDS_FOR_SETUP=${{ inputs.MAX_SECONDS_FOR_SETUP }}" >> $GITHUB_ENV + echo "_ORIGIN=github" >> $GITHUB_ENV shell: bash - name: "ALKiln: confirm info" run: | diff --git a/lib/docassemble/docassemble_api_REST.js b/lib/docassemble/docassemble_api_REST.js index 9e46fa13..faa902f4 100644 --- a/lib/docassemble/docassemble_api_REST.js +++ b/lib/docassemble/docassemble_api_REST.js @@ -27,7 +27,8 @@ da.get_dev_id = async function ( timeout ) { try { return await axios.request( options ); } catch (error) { - throw {...error.toJSON(), data: error.response?.data}; + let response = error.response || {data: null} + throw {...error.toJSON(), data: response.data}; } }; // Ends da.get_dev_id() @@ -48,7 +49,8 @@ da.create_project = async function ( project_name, timeout ) { try { return await axios.request( options ); } catch (error) { - throw {...error.toJSON(), data: error.response?.data}; + let response = error.response || {data: null} + throw {...error.toJSON(), data: response.data}; } }; // Ends da.create_project() @@ -71,7 +73,8 @@ da.pull = async function ( timeout ) { try { return await axios.request( options ); } catch (error) { - throw {...error.toJSON(), data: error.response?.data}; + let response = error.response || {data: null} + throw {...error.toJSON(), data: response.data}; } }; // Ends da.pull() @@ -92,7 +95,8 @@ da.has_task_finished = async function ( task_id, timeout ) { try { return await axios.request( options ); } catch (error) { - throw {...error.toJSON(), data: error.response?.data}; + let response = error.response || {data: null} + throw {...error.toJSON(), data: response.data}; } }; // Ends da.has_task_finished() @@ -113,7 +117,8 @@ da.delete_project = async function ( timeout ) { try { return await axios.request( options ); } catch (error) { - throw {...error.toJSON(), data: error.response?.data}; + let response = error.response || {data: null} + throw {...error.toJSON(), data: response.data}; } }; // Ends da.delete_project() @@ -142,7 +147,8 @@ da.__delete_projects_starting_with = async function ( base_name, starting_num=1, await axios.request( options ); } catch ( error ) { - console.log( {...error.toJSON(), data: error.response?.data}); + let response = error.response || {data: null} + console.log( {...error.toJSON(), data: response.data}); } name_incrementor++; } diff --git a/lib/docassemble/docassemble_api_interface.js b/lib/docassemble/docassemble_api_interface.js index d86648ad..fccbf053 100644 --- a/lib/docassemble/docassemble_api_interface.js +++ b/lib/docassemble/docassemble_api_interface.js @@ -122,9 +122,15 @@ da_i.throw_an_error_if_server_is_not_responding = async function ( options ) { // node -e 'require("./lib/docassemble/docassemble_api_interface.js").is_server_responding()' da_i.is_server_responding = async function ( dev_id_options ) { - /** Use _da_REST.get_dev_id() to test if server is up. + /** For non-playground interviews, use _da_REST.get_dev_id() to + * return if server is up. Otherwise, return true. * See https://docassemble.org/docs/api.html#user. */ + // Playground interview will itself timeout. No way to detect that. + if ( session_vars.get_origin() === 'playground' ) { + return true; + } + // Default timeouts let { timeout } = dev_id_options || { timeout: DEFAULT_MAX_REQUEST_MS }; try { @@ -141,13 +147,28 @@ da_i.is_server_responding = async function ( dev_id_options ) { // node -e 'require("./lib/docassemble/docassemble_api_interface.js").get_dev_id()' da_i.get_dev_id = async function ( dev_id_options ) { - /** For consistency, wrap _da_REST.get_dev_id(). - * See https://docassemble.org/docs/api.html#user. */ + /** Get user id, either from the server or from env vars (for Playground runs). + * See https://docassemble.org/docs/api.html#user. + * + * @param dev_id_options {obj} Options for getting dev_id + * @param dev_id_options.timeout {int} Required if options is defined. Max MS + * to allow for server request. + */ - // Default timeouts - let { timeout } = dev_id_options || { timeout: DEFAULT_MAX_REQUEST_MS }; - let response = await _da_REST.get_dev_id( timeout ); - return response.data.id; + let dev_id = null; + + // For tests running in the dev Playground, use the appropriate env var + if ( session_vars.get_origin() === 'playground' ) { + dev_id = session_vars.get_user_id(); + + } else { + // Default timeouts + let { timeout } = dev_id_options || { timeout: DEFAULT_MAX_REQUEST_MS }; + let response = await _da_REST.get_dev_id( timeout ); + dev_id = response.data.id; + } + + return dev_id; }; // Ends da_i.get_dev_id(); diff --git a/lib/run_cucumber.js b/lib/run_cucumber.js index b0c2a389..8ae56a3e 100755 --- a/lib/run_cucumber.js +++ b/lib/run_cucumber.js @@ -9,8 +9,6 @@ const cucumber_api = require('@cucumber/cucumber/api'); (async function main() { let argv = process.argv; - let just_args = argv.slice(2); - const environment = { cwd: process.cwd() }; console.log("alkiln-run: environment: %o", environment); const provided_config = { @@ -20,21 +18,36 @@ const cucumber_api = require('@cucumber/cucumber/api'); "paths": ["docassemble/*/data/sources/*.feature"], "retry": 1, } - if (just_args.length > 0) { + + + // If they're running it from an interview on their server instead of from GitHub + if ( session_vars.get_origin() === `playground` ) { + provided_config.paths.push(`/usr/share/docassemble/files/playgroundsources/${session_vars.get_user_id()}/${session_vars.get_user_project_name()}/*.feature`); + // For S3 + provided_config.paths.push(`/tmp/playgroundsources/${session_vars.get_user_id()}/${session_vars.get_user_project_name()}/*.feature`); + } + + let tags = argv.slice(2); + if ( session_vars.get_origin() === `playground` ) { + tags = session_vars.get_tags(); + } + + if (tags.length > 0) { // Cucumber expects tags in boolean expressions, like (@a0 or @a1) and @random. // If we get a list of only tags, then "or" them all together - if (just_args.every((val) => val.startsWith("@"))) { - provided_config.tags = just_args.join(' or '); + if (tags.every((val) => val.startsWith("@"))) { + provided_config.tags = tags.join(' or '); } // If we get a list of seemingly no tags, assume they meant to use tags - else if (just_args.every((val) => !val.startsWith("@"))) { - provided_config.tags = just_args.map((val) => "@" + val).join(' or '); + else if (tags.every((val) => !val.startsWith("@"))) { + provided_config.tags = tags.map((val) => "@" + val).join(' or '); } // If it's a mix of tags and no tags, it's probably a boolean expression! else { - provided_config.tags = just_args.join(' '); + provided_config.tags = tags.join(' '); } } + const { runConfiguration } = await cucumber_api.loadConfiguration({ provided: provided_config }, environment); @@ -47,7 +60,7 @@ const cucumber_api = require('@cucumber/cucumber/api'); importPaths: [], }, sources: {paths: []}}, environment); - console.log("alkiln-run: codeDir: %o", codeDir); + console.log("alkiln-run: working directory: %o", codeDir); const { success } = await cucumber_api.runCucumber({...runConfiguration, support}); console.log("alkiln-run: success: %b", success); @@ -71,4 +84,4 @@ const cucumber_api = require('@cucumber/cucumber/api'); let artifacts_path = session_vars.get_artifacts_path_name(); fs.appendFileSync( `${ artifacts_path}/${ log.debug_log_file }`, data); }); -})(); \ No newline at end of file +})(); diff --git a/lib/steps.js b/lib/steps.js index 45a2db72..0b6a5d6a 100644 --- a/lib/steps.js +++ b/lib/steps.js @@ -103,7 +103,14 @@ BeforeAll(async () => { Before(async (scenario) => { // Create browser, which can't be created in BeforeAll() where .driver doesn't exist for some reason if (!scope.browser) { - scope.browser = await scope.driver.launch({ headless: !session_vars.get_debug(), devtools: session_vars.get_debug() }); + if (session_vars.get_origin() === 'playground') { + // Will only run in the Playground outside of a sandbox. TODO: There's a + // better way to do this, though it's more complicated. See comments in + // https://github.com/SuffolkLITLab/ALKiln/issues/661 + scope.browser = await scope.driver.launch({ args: ['--no-sandbox'] }); + } else { + scope.browser = await scope.driver.launch({ headless: !session_vars.get_debug(), devtools: session_vars.get_debug() }); + } } // Clean up all previously existing pages for (const page of await scope.browser.pages()) { diff --git a/lib/utils/session_vars.js b/lib/utils/session_vars.js index bda38b3d..155a0607 100644 --- a/lib/utils/session_vars.js +++ b/lib/utils/session_vars.js @@ -23,9 +23,27 @@ module.exports = session_vars; // These default to null // TODO: How to isolate debug elsewhere so this file can use the logger? session_vars.get_debug = function () { return process.env.DEBUG || null; }; +session_vars.is_dev_env = function () { return process.env.DEV || null; } + session_vars.get_dev_api_key = function () { return process.env.DOCASSEMBLE_DEVELOPER_API_KEY || null; }; session_vars.get_repo_url = function () { return process.env.REPO_URL || null; }; -session_vars.is_dev_env = function () { return process.env.DEV || null; } + +session_vars.get_origin = function () { + /** The location from which the tests are being run. Possible + * valid return values: 'playground', 'github', or 'local'. + * Default is 'local' for ease of our own onboarding process. + */ + return process.env._ORIGIN || 'local'; +}; +session_vars.get_user_project_name = function () { return process.env._PROJECT_NAME || null; }; +session_vars.get_user_id = function () { return process.env._USER_ID || null; }; +session_vars.get_tags = function () { + if ( process.env._TAGS ) { + return process.env._TAGS.split(` `); + } else { + return []; + } +}; // More complex logic session_vars.get_server_reload_timeout = function () { @@ -130,7 +148,10 @@ session_vars.save_project_name = function ( project_name ) { }; // Ends save_project_name() session_vars.get_project_name = function () { - /* Get the name of the current docassemble Project */ + /* Get the name of the current docassemble Project */ + if ( session_vars.get_origin() === 'playground' ) { + return session_vars.get_user_project_name(); + } let json = JSON.parse( fs.readFileSync( runtime_config_path )); let project_name = json[ project_name_key ] || null; if ( session_vars.get_debug() ) { console.log( `ALKiln debug: Project name from file is "${ project_name }".` ); } @@ -174,7 +195,7 @@ session_vars.save_artifacts_path_name = function ( path_name ) { console.log( `ALKiln debug: Stored name of artifacts directory "${ path_name }" locally.` ); } return file; -}; // Ends get_project_name() +}; // Ends save_artifacts_path_name() session_vars.get_artifacts_path_name = function () { /* Get the name of the current artifacts path. */ @@ -187,48 +208,69 @@ session_vars.get_artifacts_path_name = function () { } catch ( error ) { return null; } -}; // Ends get_project_name() - -/** - * Ensure all required environment variables exist. Add each missing variable - * to an error that will be thrown at the end. - * - * @throws ReferenceError with a message for all missing variables. - */ - session_vars.validateEnvironment = function() { - /** Throw a useful error for the developer if any of the required env vars - * are missing. For most devs, these will be GitHub secrets. */ - - // Note: We are checking for BASE_URL, but we are referring it as SERVER_URL as that - // is the name used in GitHub secrets, which is what most devs will be using. +}; // Ends get_artifacts_path_name() +session_vars.validateEnvironment = function() { + /** Throw a useful error for the developer if any of the env vars + * required by absolutely all tests are missing. Gather all + * missing var names into one message to ease dev process. + * + * Rational for doing it all in here: + * 1. Easier to test + * 2. Will avoid piecemeal errors in separate locations + * + * TODO: Also validate format of variables? + * + * @throws ReferenceError with a message for all missing variables. + */ let errorMessage = ''; - // Since this code will most often be run in GitHub, I'm going to build a single error message that outlines all missing variables. Otherwise, a developer - // will have to run their pipeline an incredible number of times to discover all the missing variables. - if ( !session_vars.get_dev_api_key() ) - { - errorMessage += `\nThe DOCASSEMBLE_DEVELOPER_API_KEY GitHub secret must be defined. The DOCASSEMBLE_DEVELOPER_API_KEY is a docassemble API key you created for your server's testing account. It should have developer permissions.` - } - if ( !session_vars.get_da_server_url() ) - { + // Env vars required for any tests + if ( !session_vars.get_da_server_url() ) { errorMessage += `\nThe SERVER_URL GitHub secret must be defined. The SERVER_URL is the URL of the Docassemble Server where the tests should be run.`; } - if ( !session_vars.get_repo_url() ) - { - errorMessage += `\nThe REPO_URL GitHub secret must be defined. The REPO_URL is the URL to the Git repo whose Actions are being run.`; - } - // The branch name is always automatically defined in Github, it'll be missing only if you're running locally and missing an env var. - if ( !session_vars.get_branch_name() ) - { - errorMessage += `\nThe BRANCH_NAME environment variable must be defined. The BRANCH_NAME is the name of the repository branch that should be pulled into the docassemble project.`; - } + + let origin = session_vars.get_origin(); + if ( origin !== 'github' && origin !== 'local' && origin !== 'playground' ) { + errorMessage += `\nThe _ORIGIN environment variable should be set to a valid value automatically. If you see this error, file an issue at https://github.com/suffolkLITLab/docassemble-ALKilnInThePlayground. It must be defined as either 'playground', 'github', or 'local', but its value was ${origin}`; + + // If origin has one of the allowed values, we can do the rest of the validations + } else { + + // Env vars required for github or local runs, as both create Projects + // in the Playground and pull the repo from GitHub + if ( origin === 'github' || origin === 'local' ) { + if ( !session_vars.get_dev_api_key() ) { + errorMessage += `\nThe DOCASSEMBLE_DEVELOPER_API_KEY GitHub secret must be defined. The DOCASSEMBLE_DEVELOPER_API_KEY is a docassemble API key you created for your server's testing account. It should have developer permissions.` + } + // Github automatically gives this, but may be missing locally. + if ( !session_vars.get_repo_url() ) { + errorMessage += `\nThe REPO_URL environment variable must be defined. The REPO_URL is the URL to the GitHub repo whose Actions are being run.`; + } + // Github automatically gives this, but may be missing locally. + if ( !session_vars.get_branch_name() ) { + errorMessage += `\nThe BRANCH_NAME environment variable must be defined. The BRANCH_NAME is the name of the repository branch that should be pulled into the docassemble project.`; + } + } // ends github and local env vars + + // We're the ones ensuring these exist for Playground runs + if ( origin === 'playground' ) { + if ( !session_vars.get_user_project_name() ) { + errorMessage += `\nThe _PROJECT_NAME environment variable should automatically exist. If you see this error, file an issue at https://github.com/suffolkLITLab/docassemble-ALKilnInThePlayground. It is the name of the Project the developer chose to test.` + } + if ( !session_vars.get_user_id() ) { + errorMessage += `\nThe _USER_ID environment variable should automatically exist. If you see this error, file an issue at https://github.com/suffolkLITLab/docassemble-ALKilnInThePlayground. It is the user ID of the developer on the server.` + } + // Tags are optional + } // ends Playground env vars + + } // ends validation based on origin errorMessage = errorMessage.trim(); if (errorMessage !== '') { - throw new ReferenceError(errorMessage); + throw new ReferenceError(`ALKiln error: ${errorMessage}`); } }; - + session_vars.validateEnvironment(); diff --git a/package.json b/package.json index 988eacb8..5646d864 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@suffolklitlab/alkiln", - "version": "4.11.1", + "version": "5.0.0-playground-2", "description": "Integrated automated end-to-end testing with docassemble, puppeteer, and cucumber.", "main": "lib/index.js", "scripts": { diff --git a/tests/unit_tests/session_vars.test.js b/tests/unit_tests/session_vars.test.js index 4b700f52..224e67e9 100644 --- a/tests/unit_tests/session_vars.test.js +++ b/tests/unit_tests/session_vars.test.js @@ -4,42 +4,135 @@ const expect = chai.expect; const session_vars = require("../../lib/utils/session_vars.js"); const validateEnvironment = session_vars.validateEnvironment; +// Not going to totally support these any more. +delete process.env.BRANCH_PATH; +delete process.env.BASE_URL; + describe('When the `session_vars` object uses', function () { describe('validateEnvironment, it', function() { /** - * All required variables should be defined in .env. For reference, those variables are the following: - * - * DOCASSEMBLE_DEVELOPER_API_KEY - * BASE_URL - * REPO_URL - */ - it("successfully completes when all required env vars exist", function() { + * All required variables should be defined in .env. Different + * env vars for different test runner locations/origins. + */ + it("successfully completes when all required local env vars exist", function() { + process.env._ORIGIN = `local`; + process.env.SERVER_URL = `https://example.com`; + process.env.DOCASSEMBLE_DEVELOPER_API_KEY = `1`; + process.env.REPO_URL = `https://github.com/org/docassemble-repo`; + process.env.BRANCH_NAME = `a_branch`; + + delete process.env._PROJECT_NAME; + delete process.env._USER_ID; + session_vars.validateEnvironment(); }); + it("successfully completes when all required GitHub env vars exist", function() { + process.env._ORIGIN = `github`; + process.env.SERVER_URL = `https://example.com`; + process.env.DOCASSEMBLE_DEVELOPER_API_KEY = `1`; + process.env.REPO_URL = `https://github.com/org/docassemble-repo`; + process.env.BRANCH_NAME = `a_branch`; + + delete process.env._PROJECT_NAME; + delete process.env._USER_ID; + + session_vars.validateEnvironment(); + }); + it("successfully completes when all required Playground env vars exist", function() { + process.env._ORIGIN = `playground`; + process.env.SERVER_URL = `https://example.com`; + process.env._PROJECT_NAME = `TheProject`; + process.env._USER_ID = `1`; - /** - * This test has an ordering dependency with the test below. I'm not certain if I am getting lucky or if I need to explicitly declare the order. - */ - it("throws a reference error when any var is missing", function() { delete process.env.DOCASSEMBLE_DEVELOPER_API_KEY; + delete process.env.REPO_URL; + delete process.env.BRANCH_NAME; - expect(session_vars.validateEnvironment).to.throw(ReferenceError, /DOCASSEMBLE_DEVELOPER_API_KEY/); + session_vars.validateEnvironment(); }); /** - * Delete all environment variables before validating the variables. + * Throwing errors */ - it("names all missing variables in the error, not just one", function() { - delete process.env.DOCASSEMBLE_DEVELOPER_API_KEY; - delete process.env.BASE_URL; + it("names all missing always-required variables in the error", function() { + process.env._ORIGIN = 'playground'; + // Ensure it doesn't fail because of Playground required vars + process.env._PROJECT_NAME = `TheProject`; + process.env._USER_ID = `1`; + delete process.env.SERVER_URL; - delete process.env.REPO_URL; // This test just tests that the variable names show up in a fixed order. expect(session_vars.validateEnvironment).to.throw( ReferenceError, - /DOCASSEMBLE_DEVELOPER_API_KEY.*\n.*SERVER_URL.*\n.*REPO_URL/ + /SERVER_URL/ + ); + }); + + it("identifies when _ORIGIN has an invalid value", function() { + process.env._ORIGIN = 'invalid'; + delete process.env.SERVER_URL; + + // This test just tests that the variable names show up in a fixed order. + expect(session_vars.validateEnvironment).to.throw( + ReferenceError, + /_ORIGIN/ + ); + }); + + it("names all missing local variables in the error, not just one", function() { + // Ensure it doesn't fail because of always-required vars + process.env.SERVER_URL = `https://example.com`; + // Ensure it doesn't fail because of Playground required vars + process.env._PROJECT_NAME = `TheProject`; + process.env._USER_ID = `1`; + + process.env._ORIGIN = `local`; + delete process.env.DOCASSEMBLE_DEVELOPER_API_KEY; + delete process.env.REPO_URL; + delete process.env.BRANCH_NAME; + + // Matches message contents in any order + expect(session_vars.validateEnvironment).to.throw( + ReferenceError, + /(?=[\S\s]*REPO_URL)(?=[\S\s]*BRANCH_NAME)(?=[\S\s]*DOCASSEMBLE_DEVELOPER_API_KEY)/ + ); + }); + + it("names all missing GitHub variables in the error, not just one", function() { + // Ensure it doesn't fail because of always-required vars + process.env.SERVER_URL = `https://example.com`; + // Ensure it doesn't fail because of Playground required vars + process.env._PROJECT_NAME = `TheProject`; + process.env._USER_ID = `1`; + + process.env._ORIGIN = `github`; + delete process.env.DOCASSEMBLE_DEVELOPER_API_KEY; + delete process.env.REPO_URL; + delete process.env.BRANCH_NAME; + + expect(session_vars.validateEnvironment).to.throw( + ReferenceError, + /(?=[\S\s]*REPO_URL)(?=[\S\s]*BRANCH_NAME)(?=[\S\s]*DOCASSEMBLE_DEVELOPER_API_KEY)/ + ); + }); + + it("names all missing Playground variables in the error, not just one", function() { + // Ensure it doesn't fail because of always-required vars + process.env.SERVER_URL = `https://example.com`; + // Ensure it doesn't fail because of GitHub required vars + process.env.DOCASSEMBLE_DEVELOPER_API_KEY = `1`; + process.env.REPO_URL = `https://github.com/org/docassemble-repo`; + process.env.BRANCH_NAME = `a_branch`; + + process.env._ORIGIN = `playground`; + delete process.env._PROJECT_NAME; + delete process.env._USER_ID; + + expect(session_vars.validateEnvironment).to.throw( + ReferenceError, + /(?=[\S\s]*_PROJECT_NAME)(?=[\S\s]*_USER_ID)/ ); }); });