From c0814ad279661d121e5d82703bc868b6460045f3 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Fri, 25 Oct 2024 10:27:06 -0700 Subject: [PATCH] Almost there! --- ...rd-authentication-method-choice.choices.ts | 10 +- ...ion-wizard-authentication-method-choice.ts | 1 + ...lication-wizard-authentication-by-oauth.ts | 12 +- ...rd-authentication-by-saml-configuration.ts | 1 + ...plication-wizard-authentication-by-scim.ts | 133 +----------- .../providers/proxy/ProxyProviderForm.ts | 2 +- .../admin/providers/scim/SCIMProviderForm.ts | 198 +----------------- .../providers/scim/SCIMProviderFormForm.ts | 196 +++++++++++++++++ web/tests/pageobjects/controls.ts | 36 ++++ web/tests/specs/new-application-by-wizard.ts | 186 +++++----------- web/tests/specs/providers.ts | 63 +++--- web/tests/specs/shared-sequences.ts | 64 +++++- 12 files changed, 401 insertions(+), 501 deletions(-) create mode 100644 web/src/admin/providers/scim/SCIMProviderFormForm.ts diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts index b0f1dfa83a..fc7ed36597 100644 --- a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.choices.ts @@ -36,7 +36,7 @@ export type LocalTypeCreate = TypeCreate & { export const providerModelsList: LocalTypeCreate[] = [ { formName: "oauth2provider", - name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"), + name: msg("OAuth2/OpenID Provider"), description: msg("Modern applications, APIs and Single-page applications."), renderer: () => html``, @@ -50,7 +50,7 @@ export const providerModelsList: LocalTypeCreate[] = [ }, { formName: "ldapprovider", - name: msg("LDAP (Lightweight Directory Access Protocol)"), + name: msg("LDAP Provider"), description: msg( "Provide an LDAP interface for applications and users to authenticate against.", ), @@ -127,7 +127,7 @@ export const providerModelsList: LocalTypeCreate[] = [ }, { formName: "samlprovider", - name: msg("SAML (Security Assertion Markup Language)"), + name: msg("SAML Provider"), description: msg("Configure SAML provider manually"), renderer: () => html``, @@ -141,7 +141,7 @@ export const providerModelsList: LocalTypeCreate[] = [ }, { formName: "radiusprovider", - name: msg("RADIUS (Remote Authentication Dial-In User Service)"), + name: msg("Radius Provider"), description: msg("Configure RADIUS provider manually"), renderer: () => html``, @@ -155,7 +155,7 @@ export const providerModelsList: LocalTypeCreate[] = [ }, { formName: "scimprovider", - name: msg("SCIM (System for Cross-domain Identity Management)"), + name: msg("SCIM Provider"), description: msg("Configure SCIM provider manually"), renderer: () => html``, diff --git a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts index c4625f2420..eac762f3f9 100644 --- a/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts +++ b/web/src/admin/applications/wizard/auth-method-choice/ak-application-wizard-authentication-method-choice.ts @@ -35,6 +35,7 @@ export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSumm ? html`
0 ? selectedTypes[0] : undefined} @select=${(ev: CustomEvent) => { diff --git a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts index 09422aabe4..9b97efb8c8 100644 --- a/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts +++ b/web/src/admin/applications/wizard/methods/oauth/ak-application-wizard-authentication-by-oauth.ts @@ -1,7 +1,9 @@ import { renderForm } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormForm.js"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { msg } from "@lit/localize"; import { customElement, state } from "@lit/reactive-element/decorators.js"; +import { html } from "lit"; import { SourcesApi } from "@goauthentik/api"; import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api"; @@ -34,7 +36,15 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel { const showClientSecretCallback = (show: boolean) => { this.showClientSecret = show; }; - return renderForm(provider ?? {}, errors, this.showClientSecret, showClientSecretCallback); + return html` ${msg("Configure LDAP Provider")} + + ${renderForm( + provider ?? {}, + errors, + this.showClientSecret, + showClientSecretCallback, + )} + `; } } diff --git a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts index 9d419d8b07..cbb6845105 100644 --- a/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts +++ b/web/src/admin/applications/wizard/methods/saml/ak-application-wizard-authentication-by-saml-configuration.ts @@ -1,4 +1,5 @@ import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search"; +import { renderForm } from "@goauthentik/admin/providers/saml/SAMLProviderFormForm.js"; import { msg } from "@lit/localize"; import { customElement, state } from "@lit/reactive-element/decorators.js"; diff --git a/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts b/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts index f8036d1bcc..75d8534084 100644 --- a/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts +++ b/web/src/admin/applications/wizard/methods/scim/ak-application-wizard-authentication-by-scim.ts @@ -1,21 +1,8 @@ -import "@goauthentik/admin/applications/wizard/ak-wizard-title"; -import "@goauthentik/admin/common/ak-core-group-search"; -import "@goauthentik/admin/common/ak-crypto-certificate-search"; -import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search"; -import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { first } from "@goauthentik/common/utils"; -import "@goauthentik/components/ak-multi-select"; -import "@goauthentik/components/ak-switch-input"; -import "@goauthentik/components/ak-text-input"; -import "@goauthentik/elements/forms/FormGroup"; -import "@goauthentik/elements/forms/HorizontalFormElement"; - import { msg } from "@lit/localize"; import { customElement, state } from "@lit/reactive-element/decorators.js"; import { html } from "lit"; -import { ifDefined } from "lit/directives/if-defined.js"; -import { PaginatedSCIMMappingList, PropertymappingsApi, type SCIMProvider } from "@goauthentik/api"; +import { PaginatedSCIMMappingList, type SCIMProvider } from "@goauthentik/api"; import BaseProviderPanel from "../BaseProviderPanel"; @@ -26,125 +13,15 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel { constructor() { super(); - new PropertymappingsApi(DEFAULT_CONFIG) - .propertymappingsProviderScimList({ - ordering: "managed", - }) - .then((propertyMappings: PaginatedSCIMMappingList) => { - this.propertyMappings = propertyMappings; - }); - } - - propertyMappingConfiguration(provider?: SCIMProvider) { - const propertyMappings = this.propertyMappings?.results ?? []; - - const configuredMappings = (providerMappings: string[]) => - propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk)); - - const managedMappings = (key: string) => - propertyMappings - .filter((pm) => pm.managed === `goauthentik.io/providers/scim/${key}`) - .map((pm) => pm.pk); - - const pmUserValues = provider?.propertyMappings - ? configuredMappings(provider?.propertyMappings ?? []) - : managedMappings("user"); - - const pmGroupValues = provider?.propertyMappingsGroup - ? configuredMappings(provider?.propertyMappingsGroup ?? []) - : managedMappings("group"); - - const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]); - - return { pmUserValues, pmGroupValues, propertyPairs }; } render() { - const provider = this.wizard.provider as SCIMProvider | undefined; - const errors = this.wizard.errors.provider; - - const { pmUserValues, pmGroupValues, propertyPairs } = - this.propertyMappingConfiguration(provider); - return html`${msg("Configure SCIM Provider")}
- - - ${msg("Protocol settings")} -
- - - - -
-
- - ${msg("User filtering")} -
- - - -

- ${msg("Only sync users within the selected group.")} -

-
-
-
- - ${msg("Attribute mapping")} -
- - ${msg("Property mappings used for user mapping.")} -

- `} - >
- - ${msg("Property mappings used for group creation.")} -

- `} - >
-
-
+ ${renderForm( + (this.wizard.provider as SCIMProvider) ?? {}, + this.wizard.errors.provider, + )}
`; } } diff --git a/web/src/admin/providers/proxy/ProxyProviderForm.ts b/web/src/admin/providers/proxy/ProxyProviderForm.ts index 86c34969f1..b39e010f18 100644 --- a/web/src/admin/providers/proxy/ProxyProviderForm.ts +++ b/web/src/admin/providers/proxy/ProxyProviderForm.ts @@ -102,7 +102,7 @@ export class ProxyProviderFormPage extends BaseProviderForm { // prettier-ignore return html` - + diff --git a/web/src/admin/providers/scim/SCIMProviderForm.ts b/web/src/admin/providers/scim/SCIMProviderForm.ts index 5bc15c2bf8..bfdcd3736d 100644 --- a/web/src/admin/providers/scim/SCIMProviderForm.ts +++ b/web/src/admin/providers/scim/SCIMProviderForm.ts @@ -1,53 +1,11 @@ import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; -import { first } from "@goauthentik/common/utils"; -import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; -import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; -import "@goauthentik/elements/forms/FormGroup"; -import "@goauthentik/elements/forms/HorizontalFormElement"; -import "@goauthentik/elements/forms/Radio"; -import "@goauthentik/elements/forms/SearchSelect"; -import { msg } from "@lit/localize"; -import { TemplateResult, html } from "lit"; import { customElement } from "lit/decorators.js"; -import { ifDefined } from "lit/directives/if-defined.js"; -import { - CoreApi, - CoreGroupsListRequest, - Group, - PropertymappingsApi, - ProvidersApi, - SCIMMapping, - SCIMProvider, -} from "@goauthentik/api"; +import { ProvidersApi, SCIMProvider } from "@goauthentik/api"; -export async function scimPropertyMappingsProvider(page = 1, search = "") { - const propertyMappings = await new PropertymappingsApi( - DEFAULT_CONFIG, - ).propertymappingsProviderScimList({ - ordering: "managed", - pageSize: 20, - search: search.trim(), - page, - }); - return { - pagination: propertyMappings.pagination, - options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), - }; -} - -export function makeSCIMPropertyMappingsSelector( - instanceMappings: string[] | undefined, - defaultSelected: string, -) { - const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; - return localMappings - ? ([pk, _]: DualSelectPair) => localMappings.has(pk) - : ([_0, _1, _2, mapping]: DualSelectPair) => - mapping?.managed === defaultSelected; -} +import { renderForm } from "./SCIMProviderFormForm.js"; @customElement("ak-provider-scim-form") export class SCIMProviderFormPage extends BaseProviderForm { @@ -70,156 +28,8 @@ export class SCIMProviderFormPage extends BaseProviderForm { } } - renderForm(): TemplateResult { - return html` - - - - ${msg("Protocol settings")} -
- - -

- ${msg("SCIM base url, usually ends in /v2.")} -

-
- - - - - -

- ${msg( - "Token to authenticate with. Currently only bearer authentication is supported.", - )} -

-
-
-
- - ${msg("User filtering")} -
- - - - - => { - const args: CoreGroupsListRequest = { - ordering: "name", - includeUsers: false, - }; - if (query !== undefined) { - args.search = query; - } - const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList( - args, - ); - return groups.results; - }} - .renderElement=${(group: Group): string => { - return group.name; - }} - .value=${(group: Group | undefined): string | undefined => { - return group ? group.pk : undefined; - }} - .selected=${(group: Group): boolean => { - return group.pk === this.instance?.filterGroup; - }} - ?blankable=${true} - > - -

- ${msg("Only sync users within the selected group.")} -

-
-
-
- - ${msg("Attribute mapping")} -
- - - -

- ${msg("Property mappings used to user mapping.")} -

-
- - -

- ${msg("Property mappings used to group creation.")} -

-
-
-
`; + renderForm() { + return renderForm(this.instance ?? {}, []); } } diff --git a/web/src/admin/providers/scim/SCIMProviderFormForm.ts b/web/src/admin/providers/scim/SCIMProviderFormForm.ts new file mode 100644 index 0000000000..f6ea6bb434 --- /dev/null +++ b/web/src/admin/providers/scim/SCIMProviderFormForm.ts @@ -0,0 +1,196 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js"; +import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js"; +import "@goauthentik/elements/forms/FormGroup"; +import "@goauthentik/elements/forms/HorizontalFormElement"; +import "@goauthentik/elements/forms/Radio"; +import "@goauthentik/elements/forms/SearchSelect"; + +import { msg } from "@lit/localize"; +import { html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { + CoreApi, + CoreGroupsListRequest, + Group, + PropertymappingsApi, + SCIMMapping, + SCIMProvider, +} from "@goauthentik/api"; + +export async function scimPropertyMappingsProvider(page = 1, search = "") { + const propertyMappings = await new PropertymappingsApi( + DEFAULT_CONFIG, + ).propertymappingsProviderScimList({ + ordering: "managed", + pageSize: 20, + search: search.trim(), + page, + }); + return { + pagination: propertyMappings.pagination, + options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]), + }; +} + +export function makeSCIMPropertyMappingsSelector( + instanceMappings: string[] | undefined, + defaultSelected: string, +) { + const localMappings = instanceMappings ? new Set(instanceMappings) : undefined; + return localMappings + ? ([pk, _]: DualSelectPair) => localMappings.has(pk) + : ([_0, _1, _2, mapping]: DualSelectPair) => + mapping?.managed === defaultSelected; +} + +export function renderForm(provider?: Partial, errors: ValidationError = {}) { + return html` + + + + + + ${msg("Protocol settings")} +
+ + +

+ ${msg("SCIM base url, usually ends in /v2.")} +

+
+ + + + + +

+ ${msg( + "Token to authenticate with. Currently only bearer authentication is supported.", + )} +

+
+
+
+ + ${msg("User filtering")} +
+ + + + + => { + const args: CoreGroupsListRequest = { + ordering: "name", + includeUsers: false, + }; + if (query !== undefined) { + args.search = query; + } + const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args); + return groups.results; + }} + .renderElement=${(group: Group): string => { + return group.name; + }} + .value=${(group: Group | undefined): string | undefined => { + return group ? group.pk : undefined; + }} + .selected=${(group: Group): boolean => { + return group.pk === provider?.filterGroup; + }} + blankable + > + +

+ ${msg("Only sync users within the selected group.")} +

+
+
+
+ + + ${msg("Attribute mapping")} +
+ + +

+ ${msg("Property mappings used to user mapping.")} +

+
+ + +

+ ${msg("Property mappings used to group creation.")} +

+
+
+
+ `; +} diff --git a/web/tests/pageobjects/controls.ts b/web/tests/pageobjects/controls.ts index d84710a236..5e746fe3d7 100644 --- a/web/tests/pageobjects/controls.ts +++ b/web/tests/pageobjects/controls.ts @@ -2,6 +2,11 @@ import { browser } from "@wdio/globals"; import { match } from "ts-pattern"; import { Key } from "webdriverio"; +export async function doBlur(el: WebdriverIO.Element | ChainablePromiseElement) { + const element = await el; + browser.execute((element) => element.blur()); +} + export async function setSearchSelect(name: string, value: string) { const control = await (async () => { try { @@ -38,12 +43,14 @@ export async function setSearchSelect(name: string, value: string) { } await (await button).click(); await browser.keys(Key.Tab); + await doBlur(control); } export async function setTextInput(name: string, value: string) { const control = await $(`input[name="${name}"]`); await control.scrollIntoView(); await control.setValue(value); + await doBlur(control); } export async function setRadio(name: string, value: string) { @@ -52,6 +59,7 @@ export async function setRadio(name: string, value: string) { const item = await control.$(`label.*=${value}`).parentElement(); await item.scrollIntoView(); await item.click(); + await doBlur(control); } export async function setTypeCreate(name: string, value: string | RegExp) { @@ -73,6 +81,7 @@ export async function setTypeCreate(name: string, value: string | RegExp) { await card.scrollIntoView(); await card.click(); + await doBlur(control); } export async function setFormGroup(name: string | RegExp, setting: "open" | "closed") { @@ -95,6 +104,7 @@ export async function setFormGroup(name: string | RegExp, setting: "open" | "clo .with(["false", "open"], async () => await toggle.click()) .with(["true", "closed"], async () => await toggle.click()) .otherwise(async () => {}); + await doBlur(formGroup); } export async function clickButton(name: string, ctx?: WebdriverIO.Element) { @@ -110,4 +120,30 @@ export async function clickButton(name: string, ctx?: WebdriverIO.Element) { } await button.scrollIntoView(); await button.click(); + await doBlur(button); +} + +const tap = (a: T): T => { + console.log(a); + return a; +}; + +export async function clickToggleGroup(name: string, value: string | RegExp) { + const comparator = + typeof name === "string" + ? (sample) => tap(sample) === tap(value) + : (sample) => value.test(sample); + + const button = await (async () => { + for await (const button of $(`[data-ouid-component-name=${name}]`).$$( + ".pf-c-toggle-group__button", + )) { + if (comparator(await button.$(".pf-c-toggle-group__text").getText())) { + return button; + } + } + })(); + await button.scrollIntoView(); + await button.click(); + await doBlur(button); } diff --git a/web/tests/specs/new-application-by-wizard.ts b/web/tests/specs/new-application-by-wizard.ts index a43d84b9d7..c636fda21f 100644 --- a/web/tests/specs/new-application-by-wizard.ts +++ b/web/tests/specs/new-application-by-wizard.ts @@ -9,29 +9,42 @@ import ApplicationWizardView from "../pageobjects/application-wizard.page.js"; import ApplicationsListPage from "../pageobjects/applications-list.page.js"; import { randomId } from "../utils/index.js"; import { login } from "../utils/login.js"; +import { type TestSequence } from "./shared-sequences"; +import { + simpleForwardAuthDomainProxyProviderForm, + simpleForwardAuthProxyProviderForm, + simpleLDAPProviderForm, + simpleOAuth2ProviderForm, + simpleProxyProviderForm, + simpleRadiusProviderForm, + simpleSAMLProviderForm, + simpleSCIMProviderForm, +} from "./shared-sequences.js"; -async function reachTheProvider(title: string) { - const newPrefix = randomId(); +const SUCCESS_MESSAGE = "Your application has been saved"; +async function reachTheApplicationsPage() { await ApplicationsListPage.logout(); await login(); await ApplicationsListPage.open(); await ApplicationsListPage.pause("ak-page-header"); await expect(await ApplicationsListPage.pageHeader()).toBeDisplayed(); await expect(await ApplicationsListPage.pageHeader()).toHaveText("Applications"); +} + +async function fillOutTheApplication(title: string) { + const newPrefix = randomId(); await (await ApplicationsListPage.startWizardButton()).click(); await (await ApplicationWizardView.wizardTitle()).waitForDisplayed(); await expect(await ApplicationWizardView.wizardTitle()).toHaveText("New application"); - await (await ApplicationWizardView.app.name()).setValue(`${title} - ${newPrefix}`); await (await ApplicationWizardView.app.uiSettings()).scrollIntoView(); await (await ApplicationWizardView.app.uiSettings()).click(); await (await ApplicationWizardView.app.launchUrl()).scrollIntoView(); await (await ApplicationWizardView.app.launchUrl()).setValue("http://example.goauthentik.io"); - await (await ApplicationWizardView.nextButton()).click(); - return await ApplicationWizardView.pause(); + await ApplicationWizardView.pause(); } async function getCommitMessage() { @@ -39,136 +52,45 @@ async function getCommitMessage() { return await ApplicationWizardView.successMessage(); } -const SUCCESS_MESSAGE = "Your application has been saved"; -const EXPLICIT_CONSENT = "default-provider-authorization-explicit-consent"; +async function fillOutTheProviderAndCommit(provider: TestSequence) { + // The wizard automagically provides a name. If it doesn't, that's a bug. + const wizardProvider = provider.filter((p) => p.length < 2 || p[1] !== "name"); + await $("ak-wizard-page-type-create").waitForDisplayed(); + for await (const field of wizardProvider) { + const thefunc = field[0]; + const args = field.slice(1); + console.log(`Running ${args.join(", ")}`); + // @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it." + await thefunc.apply($, args); + await browser.pause(1000); + } -describe("Configure Applications with the Application Wizard", () => { - it("Should configure a simple LDAP Application", async () => { - await reachTheProvider("New LDAP Application"); + await $("ak-wizard-frame").$("footer button.pf-m-primary").click(); + await ApplicationWizardView.pause(); + await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE); +} - await (await ApplicationWizardView.providerList()).waitForDisplayed(); - await (await ApplicationWizardView.ldapProvider).scrollIntoView(); - await (await ApplicationWizardView.ldapProvider).click(); - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow"); - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE); +async function itShouldConfigureApplicationsViaTheWizard(name: string, provider: TestSequence) { + it(`Should successfully configure an application with a ${name} provider`, async () => { + await reachTheApplicationsPage(); + await fillOutTheApplication(name); + await fillOutTheProviderAndCommit(provider); }); +} - it("Should configure a simple Oauth2 Application", async () => { - await reachTheProvider("New Oauth2 Application"); +const providers = [ + ["LDAP", simpleLDAPProviderForm], + ["OAuth2", simpleOAuth2ProviderForm], + ["Radius", simpleRadiusProviderForm], + ["SAML", simpleSAMLProviderForm], + ["SCIM", simpleSCIMProviderForm], + ["Proxy", simpleProxyProviderForm], + ["Forward Auth (single application)", simpleForwardAuthProxyProviderForm], + ["Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm], +]; - await (await ApplicationWizardView.providerList()).waitForDisplayed(); - await (await ApplicationWizardView.oauth2Provider).scrollIntoView(); - await (await ApplicationWizardView.oauth2Provider).click(); - - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT); - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE); - }); - - it("Should configure a simple SAML Application", async () => { - await reachTheProvider("New SAML Application"); - - await (await ApplicationWizardView.providerList()).waitForDisplayed(); - await (await ApplicationWizardView.samlProvider).scrollIntoView(); - await (await ApplicationWizardView.samlProvider).click(); - - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT); - await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/"); - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE); - }); - - it("Should configure a simple SCIM Application", async () => { - await reachTheProvider("New SCIM Application"); - - await (await ApplicationWizardView.providerList()).waitForDisplayed(); - await (await ApplicationWizardView.scimProvider).scrollIntoView(); - await (await ApplicationWizardView.scimProvider).click(); - - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await ApplicationWizardView.scim.url.setValue("http://example.com:8000/"); - await ApplicationWizardView.scim.token.setValue("a-very-basic-token"); - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE); - }); - - it("Should configure a simple Radius Application", async () => { - await reachTheProvider("New Radius Application"); - - await (await ApplicationWizardView.providerList()).waitForDisplayed(); - await (await ApplicationWizardView.radiusProvider).scrollIntoView(); - await (await ApplicationWizardView.radiusProvider).click(); - - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await ApplicationWizardView.radius.setAuthenticationFlow("default-authentication-flow"); - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE); - }); - - it("Should configure a simple Transparent Proxy Application", async () => { - await reachTheProvider("New Transparent Proxy Application"); - - await (await ApplicationWizardView.providerList()).waitForDisplayed(); - await (await ApplicationWizardView.proxyProviderProxy).scrollIntoView(); - await (await ApplicationWizardView.proxyProviderProxy).click(); - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await ApplicationWizardView.transparentProxy.setAuthorizationFlow(EXPLICIT_CONSENT); - await ApplicationWizardView.transparentProxy.externalHost.setValue( - "http://external.example.com", - ); - await ApplicationWizardView.transparentProxy.internalHost.setValue( - "http://internal.example.com", - ); - - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE); - }); - - it("Should configure a simple Forward Proxy Application", async () => { - await reachTheProvider("New Forward Proxy Application"); - - await (await ApplicationWizardView.providerList()).waitForDisplayed(); - await (await ApplicationWizardView.proxyProviderForwardsingle).scrollIntoView(); - await (await ApplicationWizardView.proxyProviderForwardsingle).click(); - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT); - await ApplicationWizardView.forwardProxy.externalHost.setValue( - "http://external.example.com", - ); - - await (await ApplicationWizardView.nextButton()).click(); - await ApplicationWizardView.pause(); - - await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE); - }); +describe("Configuring Applications Via the Wizard", () => { + for (const [name, provider] of providers) { + itShouldConfigureApplicationsViaTheWizard(name, provider()); + } }); diff --git a/web/tests/specs/providers.ts b/web/tests/specs/providers.ts index 4a69453835..2e6299cd0c 100644 --- a/web/tests/specs/providers.ts +++ b/web/tests/specs/providers.ts @@ -3,10 +3,16 @@ import { expect } from "@wdio/globals"; import ProviderWizardView from "../pageobjects/provider-wizard.page.js"; import ProvidersListPage from "../pageobjects/providers-list.page.js"; import { login } from "../utils/login.js"; +import { type TestSequence } from "./shared-sequences"; import { + simpleForwardAuthDomainProxyProviderForm, + simpleForwardAuthProxyProviderForm, simpleLDAPProviderForm, simpleOAuth2ProviderForm, + simpleProxyProviderForm, simpleRadiusProviderForm, + simpleSAMLProviderForm, + simpleSCIMProviderForm, } from "./shared-sequences.js"; async function reachTheProvider() { @@ -36,56 +42,39 @@ const hasProviderSuccessMessage = async () => { timeout: 1000, timeoutMsg: "Expected to see provider success message." }, ); -type FieldDesc = [(..._: unknown) => Promise, ...unknown]; - -async function fillOutFields(fields: FieldDesc[]) { +async function fillOutFields(fields: TestSequence) { for (const field of fields) { const thefunc = field[0]; const args = field.slice(1); + // @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it." await thefunc.apply($, args); } } -describe("Configure Oauth2 Providers", () => { - it("Should configure a simple OAuth2 Provider", async () => { +async function itShouldConfigureASimpleProvider(name: string, provider: TestSequence) { + it(`Should successfully configure a ${name} provider`, async () => { await reachTheProvider(); await $("ak-wizard-page-type-create").waitForDisplayed(); - await fillOutFields(simpleOAuth2ProviderForm()); + await fillOutFields(provider); await ProviderWizardView.pause(); await ProviderWizardView.nextButton.click(); await hasProviderSuccessMessage(); }); -}); +} -describe("Configure LDAP Providers", () => { - it("Should configure a simple LDAP Provider", async () => { - await reachTheProvider(); - await $("ak-wizard-page-type-create").waitForDisplayed(); - await fillOutFields(simpleLDAPProviderForm()); - await ProviderWizardView.pause(); - await ProviderWizardView.nextButton.click(); - await hasProviderSuccessMessage(); - }); -}); +describe("Configuring Providers", () => { + const providers = [ + ["LDAP", simpleLDAPProviderForm], + ["OAuth2", simpleOAuth2ProviderForm], + ["Radius", simpleRadiusProviderForm], + ["SAML", simpleSAMLProviderForm], + ["SCIM", simpleSCIMProviderForm], + ["Proxy", simpleProxyProviderForm], + ["Forward Auth (single application)", simpleForwardAuthProxyProviderForm], + ["Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm], + ]; -describe("Configure Radius Providers", () => { - it("Should configure a simple Radius Provider", async () => { - await reachTheProvider(); - await $("ak-wizard-page-type-create").waitForDisplayed(); - await fillOutFields(simpleRadiusProviderForm()); - await ProviderWizardView.pause(); - await ProviderWizardView.nextButton.click(); - await hasProviderSuccessMessage(); - }); -}); - -describe("Configure SAML Providers", () => { - it("Should configure a simple Radius Provider", async () => { - await reachTheProvider(); - await $("ak-wizard-page-type-create").waitForDisplayed(); - await fillOutFields(simpleRadiusProviderForm()); - await ProviderWizardView.pause(); - await ProviderWizardView.nextButton.click(); - await hasProviderSuccessMessage(); - }); + for (const [name, provider] of providers) { + itShouldConfigureASimpleProvider(name, provider()); + } }); diff --git a/web/tests/specs/shared-sequences.ts b/web/tests/specs/shared-sequences.ts index 77504672ed..814a8c755c 100644 --- a/web/tests/specs/shared-sequences.ts +++ b/web/tests/specs/shared-sequences.ts @@ -1,5 +1,6 @@ import { clickButton, + clickToggleGroup, setFormGroup, setSearchSelect, setTextInput, @@ -10,14 +11,26 @@ import { randomId } from "../utils/index.js"; const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`; -export const simpleOAuth2ProviderForm = () => [ +export type TestInteraction = + | [typeof clickButton, ...Parameters] + | [typeof clickToggleGroup, ...Parameters] + | [typeof setFormGroup, ...Parameters] + | [typeof setSearchSelect, ...Parameters] + | [typeof setTextInput, ...Parameters] + | [typeof setTypeCreate, ...Parameters]; + +export type TestSequence = TestInteraction[]; + +export type TestProvider = () => TestSequence; + +export const simpleOAuth2ProviderForm: TestProvider = () => [ [setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"], [clickButton, "Next"], [setTextInput, "name", newObjectName("New Oauth2 Provider")], [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], ]; -export const simpleLDAPProviderForm = () => [ +export const simpleLDAPProviderForm: TestProvider = () => [ [setTypeCreate, "selectProviderType", "LDAP Provider"], [clickButton, "Next"], [setTextInput, "name", newObjectName("New LDAP Provider")], @@ -27,9 +40,54 @@ export const simpleLDAPProviderForm = () => [ [setSearchSelect, "invalidationFlow", "default-invalidation-flow"], ]; -export const simpleRadiusProviderForm = () => [ +export const simpleRadiusProviderForm: TestProvider = () => [ [setTypeCreate, "selectProviderType", "Radius Provider"], [clickButton, "Next"], [setTextInput, "name", newObjectName("New Radius Provider")], [setSearchSelect, "authorizationFlow", "default-authentication-flow"], ]; + +export const simpleSAMLProviderForm: TestProvider = () => [ + [setTypeCreate, "selectProviderType", "SAML Provider"], + [clickButton, "Next"], + [setTextInput, "name", newObjectName("New SAML Provider")], + [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], + [setTextInput, "acsUrl", "http://example.com:8000/"], +]; + +export const simpleSCIMProviderForm: TestProvider = () => [ + [setTypeCreate, "selectProviderType", "SCIM Provider"], + [clickButton, "Next"], + [setTextInput, "name", newObjectName("New SCIM Provider")], + [setTextInput, "url", "http://example.com:8000/"], + [setTextInput, "token", "insert-real-token-here"], +]; + +export const simpleProxyProviderForm: TestProvider = () => [ + [setTypeCreate, "selectProviderType", "Proxy Provider"], + [clickButton, "Next"], + [setTextInput, "name", newObjectName("New Proxy Provider")], + [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], + [clickToggleGroup, "proxy-type-toggle", "Proxy"], + [setTextInput, "externalHost", "http://example.com:8000/"], + [setTextInput, "internalHost", "http://example.com:8001/"], +]; + +export const simpleForwardAuthProxyProviderForm: TestProvider = () => [ + [setTypeCreate, "selectProviderType", "Proxy Provider"], + [clickButton, "Next"], + [setTextInput, "name", newObjectName("New Forward Auth Provider")], + [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], + [clickToggleGroup, "proxy-type-toggle", "Forward auth (single application)"], + [setTextInput, "externalHost", "http://example.com:8000/"], +]; + +export const simpleForwardAuthDomainProxyProviderForm: TestProvider = () => [ + [setTypeCreate, "selectProviderType", "Proxy Provider"], + [clickButton, "Next"], + [setTextInput, "name", newObjectName("New Forward Auth Domain Level Provider")], + [setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"], + [clickToggleGroup, "proxy-type-toggle", "Forward auth (domain level)"], + [setTextInput, "externalHost", "http://example.com:8000/"], + [setTextInput, "cookieDomain", "somedomain.tld"], +];