From 5d874cb3151d3378181903bfb0dc9498c28cf311 Mon Sep 17 00:00:00 2001 From: Ken Sternberg Date: Wed, 13 Nov 2024 10:57:33 -0800 Subject: [PATCH] web/admin/better-footer-links # What - Remove the "JSON or YAML" language from the AdminSettings page for describing FooterLinks inputs. - Add unit tests for ArrayInput and AdminSettingsFooterLinks. - Provide a property for accessing a component's value # Why Providing a property by which the JSONified version of the value can be accessed enhances the ability of tests to independently check that the value is in a state we desire, since properties can easily be accessed across the wire protocol used by browser-based testing environments. --- .../AdminSettingsFooterLinks.ts | 9 +-- .../admin/admin-settings/AdminSettingsForm.ts | 3 +- .../stories/AdminSettingsFooterLinks.test.ts | 68 +++++++++++++++++++ web/src/elements/AkControlElement.ts | 4 ++ web/src/elements/tests/ak-array-input.test.ts | 55 +++++++++++++++ 5 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 web/src/admin/admin-settings/stories/AdminSettingsFooterLinks.test.ts create mode 100644 web/src/elements/tests/ak-array-input.test.ts diff --git a/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts b/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts index a2e620c10c7e..5ea882ecd2d6 100644 --- a/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts +++ b/web/src/admin/admin-settings/AdminSettingsFooterLinks.ts @@ -17,6 +17,10 @@ export interface IFooterLinkInput { footerLink: FooterLink; } +const LEGAL_SCHEMES = ["http://", "https://", "mailto:"]; +const hasLegalScheme = (url: string) => + LEGAL_SCHEMES.some((scheme) => url.substr(0, scheme.length).toLowerCase() === scheme); + @customElement("ak-admin-settings-footer-link") export class FooterLinkInput extends AkControlElement { static get styles() { @@ -50,10 +54,7 @@ export class FooterLinkInput extends AkControlElement { get isValid() { const href = this.json()?.href ?? ""; - return ( - (href.substr(0, 7) === "http://" || href.substr(0, 8) === "https://") && - URL.canParse(href) - ); + return hasLegalScheme(href) && URL.canParse(href); } render() { diff --git a/web/src/admin/admin-settings/AdminSettingsForm.ts b/web/src/admin/admin-settings/AdminSettingsForm.ts index 70d290c503ec..38c9ab4f2092 100644 --- a/web/src/admin/admin-settings/AdminSettingsForm.ts +++ b/web/src/admin/admin-settings/AdminSettingsForm.ts @@ -188,9 +188,8 @@ export class AdminSettingsForm extends Form {

${msg( - "This option configures the footer links on the flow executor pages. It must be a valid YAML or JSON list and can be used as follows:", + "This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.", )} - [{"name": "Link Name","href":"https://goauthentik.io"}]

{ + afterEach(async () => { + await browser.execute(async () => { + await document.body.querySelector("ak-admin-settings-footer-link")?.remove(); + if (document.body["_$litPart$"]) { + // @ts-expect-error expression of type '"_$litPart$"' is added by Lit + await delete document.body["_$litPart$"]; + } + }); + }); + + it("should render an empty control", async () => { + render(html``); + const link = await $("ak-admin-settings-footer-link"); + await expect(await link.getProperty("isValid")).toStrictEqual(false); + await expect(await link.getProperty("toJson")).toEqual({ name: "", href: "" }); + }); + + it("should not be valid if just a name is filled in", async () => { + render(html``); + const link = await $("ak-admin-settings-footer-link"); + await link.$('input[name="name"]').setValue("foo"); + await expect(await link.getProperty("isValid")).toStrictEqual(false); + await expect(await link.getProperty("toJson")).toEqual({ name: "foo", href: "" }); + }); + + it("should be valid if just a URL is filled in", async () => { + render(html``); + const link = await $("ak-admin-settings-footer-link"); + await link.$('input[name="href"]').setValue("https://foo.com"); + await expect(await link.getProperty("isValid")).toStrictEqual(true); + await expect(await link.getProperty("toJson")).toEqual({ + name: "", + href: "https://foo.com", + }); + }); + + it("should be valid if both are filled in", async () => { + render(html``); + const link = await $("ak-admin-settings-footer-link"); + await link.$('input[name="name"]').setValue("foo"); + await link.$('input[name="href"]').setValue("https://foo.com"); + await expect(await link.getProperty("isValid")).toStrictEqual(true); + await expect(await link.getProperty("toJson")).toEqual({ + name: "foo", + href: "https://foo.com", + }); + }); + + it("should not be valid if the URL is not valid", async () => { + render(html``); + const link = await $("ak-admin-settings-footer-link"); + await link.$('input[name="name"]').setValue("foo"); + await link.$('input[name="href"]').setValue("never://foo.com"); + await expect(await link.getProperty("toJson")).toEqual({ + name: "foo", + href: "never://foo.com", + }); + await expect(await link.getProperty("isValid")).toStrictEqual(false); + }); +}); diff --git a/web/src/elements/AkControlElement.ts b/web/src/elements/AkControlElement.ts index 5c389f3b6f45..984d5504e8ef 100644 --- a/web/src/elements/AkControlElement.ts +++ b/web/src/elements/AkControlElement.ts @@ -18,6 +18,10 @@ export class AkControlElement extends AKElement { throw new Error("Controllers using this protocol must override this method"); } + get toJson(): T { + return this.json(); + } + get isValid(): boolean { return true; } diff --git a/web/src/elements/tests/ak-array-input.test.ts b/web/src/elements/tests/ak-array-input.test.ts new file mode 100644 index 000000000000..214178ff87ed --- /dev/null +++ b/web/src/elements/tests/ak-array-input.test.ts @@ -0,0 +1,55 @@ +import "@goauthentik/admin/admin-settings/AdminSettingsFooterLinks.js"; +import { render } from "@goauthentik/elements/tests/utils.js"; +import { $, expect } from "@wdio/globals"; + +import { html } from "lit"; + +import { FooterLink } from "@goauthentik/api"; + +import "../ak-array-input.js"; + +const sampleItems: FooterLink[] = [ + { name: "authentik", href: "https://goauthentik.io" }, + { name: "authentik docs", href: "https://docs.goauthentik.io/docs/" }, +]; + +describe("ak-array-input", () => { + afterEach(async () => { + await browser.execute(async () => { + await document.body.querySelector("ak-array-input")?.remove(); + if (document.body["_$litPart$"]) { + // @ts-expect-error expression of type '"_$litPart$"' is added by Lit + await delete document.body["_$litPart$"]; + } + }); + }); + + const component = (items: FooterLink[] = []) => + render( + html` ({ name: "", href: "" })} + .row=${(f?: FooterLink) => + html` + `} + validate + >`, + ); + + it("should render an empty control", async () => { + await component(); + const link = await $("ak-array-input"); + await browser.pause(500); + await expect(await link.getProperty("isValid")).toStrictEqual(true); + await expect(await link.getProperty("toJson")).toEqual([]); + }); + + it("should render a populated component", async () => { + await component(sampleItems); + const link = await $("ak-array-input"); + await browser.pause(500); + await expect(await link.getProperty("isValid")).toStrictEqual(true); + await expect(await link.getProperty("toJson")).toEqual(sampleItems); + }); +});