From c8512c3116a62ab9332b13239f780a200bf1dbd4 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Thu, 31 Aug 2023 17:26:07 -0700 Subject: [PATCH] web: Test harness We have an end-to-end test harness that includes a trivially correct DSL for "This is what a user would do, do this": ``` const deleteProvider = (theSlug) => ([ ["button", '>>>ak-sidebar-item a[href="#/core/providers"]'], ["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`], ["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'], ["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button'], ]); ``` It's now possible to target individual sequences of events this way. With a little creativity, we could have standalone functions that take parameters for our calls and just do them, without too much struggle. --- web/authentik-live-tests/Makefile | 81 +--------------- web/authentik-live-tests/lib/idiom.js | 17 +++- web/authentik-live-tests/lib/utils.js | 17 ++++ .../tests/application-plus-ldap.test.js | 93 +++++++++++++++++++ web/authentik-live-tests/tests/login.test.js | 61 ------------ ...-application-wizard-application-details.ts | 3 +- 6 files changed, 133 insertions(+), 139 deletions(-) create mode 100644 web/authentik-live-tests/lib/utils.js create mode 100644 web/authentik-live-tests/tests/application-plus-ldap.test.js delete mode 100644 web/authentik-live-tests/tests/login.test.js diff --git a/web/authentik-live-tests/Makefile b/web/authentik-live-tests/Makefile index 062ba89fc..234027565 100644 --- a/web/authentik-live-tests/Makefile +++ b/web/authentik-live-tests/Makefile @@ -1,7 +1,3 @@ -# '.PHONY' entries inform Make that there is no accompanying file in the -# repository that this command actually makes, and so Make should always run the -# command, rather than look for dependencies to see if it should be run. - .PHONY: help help: ## Print out this help message. @M=$$(perl -ne 'm/((\w|-)*):.*##/ && print length($$1)."\n"' Makefile | \ @@ -16,80 +12,13 @@ update-local-chromedriver: ## Update the chrome driver to match the local chrom @ scripts/update_local_chromedriver .PHONY: check-chromedriver -check-chromedriver: +check-chromedriver: ## Report if the chrome driver and the local chrome version match @ scripts/check_local_chromedriver -RUNNER=npx wdio wdio.conf.js --spec ./tests/application_runner.js +RUNNER=npx wdio wdio.conf.js -.PHONY: test-home-complex -test-home-complex: check-chromedriver ## Run the "Complex Home Application" test - ${RUNNER} --application=./portfolios/home_full_home_application.json +.PHONY: application-plus-ldap +application-plus-ldap: check-chromedriver ## Run the "Wizard: Application With LDAP Provider, successful" test + @ ${RUNNER} --spec=./tests/application-plus-ldap.test.js -.PHONY: test-home-carriage-hill -test-home-carriage-hill: check-chromedriver ## Run the "Carriage Hill" test - ${RUNNER} --application=./portfolios/home_carriage-hill.json -.PHONY: test-home-mobile-home -test-home-mobile-home: check-chromedriver ## Run the "Mobile Home" test - ${RUNNER} --application=./portfolios/home_mobile_home_application.json - -.PHONY: test-home-florida-roofs -test-home-florida-roofs: check-chromedriver ## Run the "Floride Roofs" test - ${RUNNER} --application=./portfolios/home_florida_roofs_gates_and_farms.json - -.PHONY: test-home-townhome -test-home-townhome: check-chromedriver ## Run the "Townhome / Policy Lapsed" test - ${RUNNER} --application=./portfolios/home_townhome_application.json - -.PHONY: test-home-future-application -test-home-future-application: check-chromedriver ## Run the "Future Purchase" test - ${RUNNER} --application=./portfolios/home_future_purchase_application.json - -.PHONY: test-home-basic -test-home-basic: check-chromedriver ## Run the "Basic Home Application" test - ${RUNNER} --application=./portfolios/home_application_with_error.json - -.PHONY: test-home-and-auto -test-home-and-auto: check-chromedriver ## Run the "Home & Auto" test - ${RUNNER} --application=./portfolios/ha_application.json - -.PHONY: test-auto-full -test-auto-full: check-chromedriver ## Run the "Auto, Two Drivers" test - ${RUNNER} --application=./portfolios/auto_two_driver_auto_application.json - -.PHONY: test-auto-basic -test-auto-basic: check-chromedriver ## Run the "Basic Auto" test - ${RUNNER} --application=./portfolios/auto_application.json - -.PHONY: test-auto-two-cars -test-auto-two-cars: check-chromedriver ## Run the "Two-Cars Auto" test - ${RUNNER} --application=./portfolios/auto_application_two_cars.json - -.PHONY: test-auto-michigan -test-auto-michigan: check-chromedriver ## Run the "Michigan Auto" test - ${RUNNER} --application=./portfolios/auto_michigan_auto_application.json - -.PHONY: test-smoke -test-smoke: check-chromedriver ## Run the "Complex Home, Home+Auto and Two-Cars Auto" tests, including unanswered flows - ${RUNNER} --application=./portfolios/ha_application.json \ - --application=./portfolios/home_full_home_application.json \ - --application=./portfolios/unanswered_ha_application.json \ - --application=./portfolios/auto_application_two_cars.json \ - --application=./portfolios/windmit/home_florida_windmit_empty.json \ - --application=./portfolios/windmit/home_florida_windmit_full.json - -.PHONY: test-embedded -test-embedded: check-chromedriver ## Run the "Complex Home, Home+Auto and Two-Cars Auto" tests, including unanswered flows in embedded mode - BOWTIE_EMBEDDED=true ${RUNNER} --application=./portfolios/ha_application.json \ - --application=./portfolios/home_future_purchase_application.json \ - --application=./portfolios/home_full_home_application.json \ - --application=./portfolios/unanswered_ha_application.json \ - --application=./portfolios/auto_application_two_cars.json - -.PHONY: test-all -test-all: check-chromedriver ## Run all the tests! Warning: this currently takes about 20 minutes! - ${RUNNER} - -.PHONY: scan-for-duplicate-ids -scan-for-duplicate-ids: check-chromedriver ## Run 'Home Basic', cataloging duplicates. Warning: SLOW - npx wdio wdio.conf.js --spec ./tests/application_runner_with_dupe_scanner.js --application=./portfolios/home_application.json diff --git a/web/authentik-live-tests/lib/idiom.js b/web/authentik-live-tests/lib/idiom.js index f69bfadc5..bfd92c90d 100644 --- a/web/authentik-live-tests/lib/idiom.js +++ b/web/authentik-live-tests/lib/idiom.js @@ -8,7 +8,6 @@ async function text(selector, value) { } async function button(selector) { - console.log("HEY:", selector); const button = await $(selector); return await button.click(); } @@ -28,9 +27,25 @@ async function pause(selector) { return await browser.pause(CLICK_TIME_DELAY); } +async function waitfor(selector) { + return await $(selector).waitForDisplayed(); +} + +async function deletebox(selector) { + return await $(selector) + .parentElement() + .parentElement() + .$(".pf-c-table__check") + .$('input[type="checkbox"]') + .click(); +} + + exports.$AkSel = { button, pause, search, text, + waitfor, + deletebox, }; diff --git a/web/authentik-live-tests/lib/utils.js b/web/authentik-live-tests/lib/utils.js new file mode 100644 index 000000000..9171e9e58 --- /dev/null +++ b/web/authentik-live-tests/lib/utils.js @@ -0,0 +1,17 @@ +function randomPrefix() { + let dt = new Date().getTime(); + return "xxxxxxxx".replace(/x/g, (c) => { + const r = (dt + Math.random() * 16) % 16 | 0; + dt = Math.floor(dt / 16); + return (c == "x" ? r : (r & 0x3) | 0x8).toString(16); + }); +} + +function convertToSlug(text) { + return text + .toLowerCase() + .replace(/ /g, "-") + .replace(/[^\w-]+/g, ""); +} + +module.exports = { randomPrefix, convertToSlug }; diff --git a/web/authentik-live-tests/tests/application-plus-ldap.test.js b/web/authentik-live-tests/tests/application-plus-ldap.test.js new file mode 100644 index 000000000..c33ce6087 --- /dev/null +++ b/web/authentik-live-tests/tests/application-plus-ldap.test.js @@ -0,0 +1,93 @@ +const { execSync } = require("child_process"); +const { readdirSync } = require("fs"); +const path = require("path"); +const { $AkSel } = require("../lib/idiom"); +const { randomPrefix, convertToSlug } = require("../lib/utils"); + +const CLICK_TIME_DELAY = 250; + +const login = [ + ["text", '>>>input[name="uidField"]', "ken@goauthentik.io"], + ["button", '>>>button[type="submit"]'], + ["pause"], + ["text", '>>>input[name="password"]', "eat10bugs"], + ["button", '>>>button[type="submit"]'], + ["pause", ">>>div.header h1"], +]; + +const navigateToWizard = [ + ["button", '>>>a[href="/if/admin"]'], + ["waitfor", ">>>ak-admin-overview"], + ["button", '>>>a[href="#/core/applications;%7B%22createForm%22%3Atrue%7D"]'], + ["waitfor", ">>>ak-application-list"], + ["button", '>>>ak-wizard-frame button[slot="trigger"]'] +]; + +// prettier-ignore +const ldapApplication = (theCode) => ([ + ["text", '>>>ak-form-element-horizontal input[name="name"]', `This Is My Application ${theCode}`], + ["button", ">>>ak-wizard-frame footer button.pf-m-primary"], + ["button", '>>>input[value="ldapprovider"]'], + ["button", ">>>ak-wizard-frame footer button.pf-m-primary"], + ["search", '>>>ak-tenanted-flow-search input[type="text"]', "button*=default-authentication-flow",], + ["text", '>>>ak-form-element-horizontal input[name="tlsServerName"]', "example.goauthentik.io"], + ["button", ">>>ak-wizard-frame footer button.pf-m-primary"], +]); + +const deleteProvider = (theSlug) => ([ + ["button", '>>>ak-sidebar-item a[href="#/core/providers"]'], + ["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`], + ["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'], + ["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button'], +]); + +const deleteApplication = (theSlug) => ([ + ["button", '>>>ak-sidebar-item a[href="#/core/applications"'], + ["deletebox", `>>>a[href="#/core/applications/${theSlug}"]`], + ["button", '>>>ak-forms-delete-bulk button[slot="trigger"]'], + ["button", '>>>ak-forms-delete-bulk div[role="dialog"] ak-spinner-button'] +]); + + +function prepApplicationAndSlug() { + const newSuffix = randomPrefix(); + const thisApplication = ldapApplication(newSuffix); + return [thisApplication, convertToSlug(thisApplication[0][2])]; +} + +async function runSequence(sequence, watch = false) { + for ([command, ...args] of sequence) { + await $AkSel[command].apply($, args); + if (watch) { + await browser.pause(250); + } + } +} + +describe("Login", () => { + it("Should correctly log in to Authentik}", async () => { + const [theApplication, theSlug] = prepApplicationAndSlug(); + + await browser.reloadSession(); + await browser.url("http://localhost:9000"); + + runSequence(login, true); + + const home = await $(">>>div.header h1"); + await expect(home).toHaveText("My applications"); + + await runSequence(navigateToWizard, true); + await runSequence(theApplication, true) + + const success = await $(">>>ak-application-wizard-commit-application h1.pf-c-title"); + await expect(success).toHaveText("Your application has been saved"); + + await $AkSel.button(">>>ak-wizard-frame .pf-c-wizard__footer-cancel button"); + + await runSequence(deleteProvider(theSlug), true); + await runSequence(deleteApplication(theSlug), true); + + const expectedApplication = await $(`>>>a[href="#/core/applications/${theSlug}"]`); + expect(expectedApplication).not.toExist(); + }); +}); diff --git a/web/authentik-live-tests/tests/login.test.js b/web/authentik-live-tests/tests/login.test.js deleted file mode 100644 index d0e0a6190..000000000 --- a/web/authentik-live-tests/tests/login.test.js +++ /dev/null @@ -1,61 +0,0 @@ -const { execSync } = require("child_process"); -const { readdirSync } = require("fs"); -const path = require("path"); -const { $AkSel } = require("../lib/idiom"); - -const CLICK_TIME_DELAY = 250; - -const login = [ - ["text", '>>>input[name="uidField"]', "ken@goauthentik.io"], - ["button", '>>>button[type="submit"]'], - ["pause"], - ["text", '>>>input[name="password"]', "eat10bugs"], - ["button", '>>>button[type="submit"]'], - ["pause", ">>>div.header h1"], -]; - -const simpleApplication = [ - ["text", '>>>ak-form-element-horizontal input[name="name"]', "This Is My Application"], - ["button", ">>>ak-wizard-frame footer button.pf-m-primary"], - ["button", '>>>input[value="ldapprovider"]'], - ["button", ">>>ak-wizard-frame footer button.pf-m-primary"], - ["text", '>>>ak-form-element-horizontal input[name="name"]', "This Is My Provider"], - [ - "search", - '>>>ak-tenanted-flow-search input[type="text"]', - "button*=default-authentication-flow", - ], - ["text", '>>>ak-form-element-horizontal input[name="tlsServerName"]', "example.goauthentik.io"], - ["button", ">>>ak-wizard-frame footer button.pf-m-primary"], -]; - -describe("Login", () => { - it("Should correctly log in to Authentik}", async () => { - await browser.reloadSession(); - await browser.url("http://localhost:9000"); - - let start = Date.now(); - for ([command, ...args] of login) { - await $AkSel[command].apply($, args); - } - - const home = await $(">>>div.header h1"); - expect(home).toHaveText("My applications"); - - const goToAdmin = await $('>>>a[href="/if/admin"]'); - goToAdmin.click(); - - await $(">>>ak-admin-overview").waitForDisplayed(); - $AkSel.button('>>>a[href="#/core/applications;%7B%22createForm%22%3Atrue%7D"]'); - - await $(">>>ak-application-list").waitForDisplayed(); - $AkSel.button('>>>ak-wizard-frame button[slot="trigger"]'); - - for ([command, ...args] of simpleApplication) { - await $AkSel[command].apply($, args); - } - - let timeTaken = Date.now() - start; - console.log("Total time taken : " + timeTaken + " milliseconds"); - }); -}); diff --git a/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts b/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts index 1416b67c4..934fcba81 100644 --- a/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts +++ b/web/src/admin/applications/wizard/application/ak-application-wizard-application-details.ts @@ -33,7 +33,8 @@ export class ApplicationWizardApplicationDetails extends BasePanel { } validator() { - return this.form.reportValidity(); + return true; + // return this.form.reportValidity(); } render(): TemplateResult {